# Mailaura REST API v1 Documentation

**Base URL:** `https://www.mailaura.io/api/v1`

**Version:** 1.0

## Table of Contents

1. [Authentication](#authentication)
2. [Response Format](#response-format)
3. [Pagination](#pagination)
4. [Error Handling](#error-handling)
5. [Rate Limiting](#rate-limiting)
6. [Endpoints](#endpoints)
   - [Transactional Emails](#transactional-emails)
   - [Campaigns](#campaigns)
   - [Gallery Templates](#gallery-templates)
   - [Campaign Template Integration](#campaign-template-integration)
   - [AI Content](#ai-content)
   - [RSS Campaigns](#rss-campaigns)
   - [Automations](#automations)
   - [Letters](#letters)
   - [Tags (NEW)](#tags)
   - [Media (NEW)](#media)
   - [Brand (NEW)](#brand)
   - [Analytics (NEW)](#analytics)
   - [Profile (NEW)](#profile)
   - [API Keys (NEW)](#api-keys)
   - [Lists](#lists)
   - [Subscribers](#subscribers)
   - [Templates](#templates)
   - [Webhooks](#webhooks)

---

## Authentication

All API requests require authentication via API key. You can create API keys in your dashboard under **Settings → API Keys**.

### Headers

Include your API key in one of the following ways:

```http
X-API-Key: your_api_key_here
```

or

```http
Authorization: Bearer your_api_key_here
```

### Scopes

API keys can have different permission scopes:

| Scope | Description |
|-------|-------------|
| `read` | Read-only access to resources |
| `write` | Create, update, and delete resources |
| `send` | Send campaigns where the endpoint requires this scope |
| `domains:read`, `domains:write` | Domain-specific access; routes also accept general `read`/`write` |
| `letters:read`, `letters:write` | Letter-specific access; routes also accept general `read`/`write` |
| `*` | Full access; use only when necessary |

Scopes are endpoint-specific. A standard `write` scope does not automatically grant `read` or `send`. Transactional credentials use `send`, `read`, `templates` and `webhooks:write`; they are not standard newsletter API keys.

### Example Request

```bash
curl -X GET "https://www.mailaura.io/api/v1/campaigns" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Response Format

Most endpoints return JSON. `GET /letters/:id/pdf` returns a private raw PDF. Some integration callbacks and transactional operations use a specialized response shape; follow the endpoint-specific examples.

### Success Response

```json
{
  "success": true,
  "data": { ... }
}
```

### Paginated Response

```json
{
  "success": true,
  "data": [ ... ],
  "pagination": {
    "total": 100,
    "page": 1,
    "limit": 50,
    "totalPages": 2,
    "hasMore": true
  }
}
```

### Error Response

```json
{
  "error": "Error description",
  "timestamp": "2026-10-22T08:00:00.000Z",
  "details": { "field": "Optional validation details" }
}
```

---

## Pagination

Most list endpoints use the page parameters below. Exceptions include `/emails` (opaque `next_cursor`, limit up to 200), `/letters/campaigns` (fixed 25 per page) and `/letters/templates` (default 20, max 50). Check each operation before assuming the defaults:

| Parameter | Type | Default | Max | Description |
|-----------|------|---------|-----|-------------|
| `page` | integer | 1 | - | Page number |
| `limit` | integer | 50 | 100 | Items per page |

---

## Error Handling

### HTTP Status Codes

| Code | Description |
|------|-------------|
| `200` | Success |
| `201` | Created |
| `400` | Bad Request - Invalid input |
| `401` | Unauthorized - Invalid or missing API key |
| `403` | Forbidden - Insufficient permissions, or the organization is suspended (see below) |
| `404` | Not Found |
| `409` | Conflict - Resource already exists |
| `429` | Too Many Requests - Retry after the indicated seconds |
| `500` | Internal Server Error |

### Suspended organizations

If Mailaura has suspended an organization, its API keys become **read-only**. `GET`, `HEAD` and `OPTIONS` requests keep working, so existing data can still be read and exported. Every other method (`POST`, `PUT`, `PATCH`, `DELETE`) on an API-key-authenticated `/api/v1/*` endpoint is rejected before validation, plan checks and rate limiting:

```json
{
  "error": "This organization is suspended. The API is read-only until the suspension is lifted. Please contact support.",
  "timestamp": "2026-09-18T12:22:35.769Z"
}
```

Status code `403`. Retrying or creating a new API key does not help; contact support.

Two lanes do not use an API key and behave slightly differently: the signature-authenticated WooCommerce webhook (`POST /api/v1/woocommerce/webhook`) answers the same `403` with `code: "ORG_SUSPENDED"` (WooCommerce disables a webhook after repeated failed deliveries, so it may have to be re-enabled in the shop once the suspension is lifted), and Transactional Credentials (`X-Mailaura-Key` + `X-Mailaura-Secret`) are rejected entirely for a suspended organization — including reads — with `401` "Sending is suspended for this organization".

The dashboard applies the same rule to sender domains: a suspended organization can still list and inspect its domains, but deleting (since #409 — its rows keep others from re-adding the domain), adding a domain, re-triggering or checking verification, and changing MAIL FROM or DMARC settings are rejected with `FORBIDDEN` before any SES request, so the suspended organization cannot re-create an SES identity removed during a shutdown from the UI; the Domain Connect return path is blocked the same way (#392).

The block also covers other organizations (#409): nobody can add, verify or re-trigger a domain — its parents, subdomains, or another domain under the same registrable domain — that a suspended organization proved control of (verified at least once, not a lost claim; a domain it merely typed in blocks nobody). `POST /api/v1/domains` and `POST /api/v1/domains/{id}/verify` answer `403` "This domain cannot be added or verified. Please contact support." (deliberately generic), the dashboard answers `FORBIDDEN`, and the domain-health cron does not re-trigger SES for such a domain but moves it to pending. A suspended agency cannot create or invite client organizations. The block lives on the suspended organization's rows; making it survive organization or account deletion is #414. A domain whose first verified claim belongs to another, unsuspended organization is not blocked by this rule — when shutting down abuse, suspend every organization that shares the domain.

---

## Rate Limiting

Standard API requests use a shared organization limit per minute: Free 60, Starter 300, Pro 1,000 and Business 3,000; unknown/custom plan IDs fall back to 60. These numbers do not grant API access: the plan entitlement is checked separately, normally requiring Pro or Business. Additional per-operation limits may apply, including 30/minute for letter sends and 60/minute for letter previews.

API requests are rate-limited based on your subscription plan. Critical API, billing, and auth request limits use Redis so limits are shared across serverless instances. If Redis is unavailable, rate limiting fails open and logs a warning instead of falling back to per-instance memory counters.

A 429 response includes `Retry-After` and the `retryAfter` JSON value. Some endpoint-specific limit responses also include the following headers; they are not guaranteed on every successful response:

```http
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1699999999
Retry-After: 60
```

---

## Endpoints

---

# Transactional Emails

Send transactional emails (password resets, order confirmations, notifications, etc.) via HTTP API.

> **Note:** Transactional emails use separate API credentials (`ml_trans_live_*` / `ml_trans_test_*`).
> Create them in **Settings → Transactional Emails → API Keys**.

## Authentication

Transactional API uses an API Key / API Secret pair:

```http
X-Mailaura-Key: YOUR_API_KEY
X-Mailaura-Secret: YOUR_API_SECRET
```

Or as Bearer token:

```http
Authorization: Bearer YOUR_API_KEY:YOUR_API_SECRET
```

### Sandbox Mode

Keys starting with `ml_trans_test_` are sandbox keys:
- Emails are logged but **not sent** to recipients
- Useful for testing integrations without sending real emails
- Response includes `"sandbox": true`

---

## POST /send

SMTP relay is currently disabled. Use this HTTP API or the WordPress plugin HTTP delivery route; do not configure `smtp.mailaura.io`.

Send a transactional email.

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | string | ✅ | Recipient email address |
| `from` | string | ❌ | Sender email (uses org default if omitted) |
| `subject` | string | ✅ | Email subject line |
| `html` | string | ❌* | HTML content |
| `text` | string | ❌* | Plain text content |
| `template_slug` | string | ❌* | Template identifier (e.g., "password-reset") |
| `template_variables` | object | ❌ | Variables for template rendering |
| `language` | string | ❌ | Language code for template (default: "de") |
| `priority` | string | ❌ | "critical", "high", "normal", "low" |
| `tags` | array | ❌ | Tags for categorization |
| `metadata` | object | ❌ | Custom metadata (stored, returned in webhooks) |
| `attachments` | array | ❌ | File attachments (max 10, max 25MB total) |

*At least one of `html`, `text`, or `template_slug` is required.

### Attachment Object

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `filename` | string | ✅ | Filename shown to recipient (max 255 chars) |
| `content` | string | ✅ | Base64-encoded file content (max 10MB per file) |
| `contentType` | string | ❌ | MIME type (default: "application/octet-stream") |
| `contentId` | string | ❌ | Content-ID for inline attachments (use in HTML as `cid:contentId`) |
| `disposition` | string | ❌ | "attachment" (default) or "inline" |

**Allowed MIME Types:**
- Documents: `application/pdf`, `application/msword`, `.docx`, `.xlsx`, `.odt`, `.ods`
- Images: `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/svg+xml`
- Text: `text/plain`, `text/csv`
- Archives: `application/zip`

### Priority Levels

| Priority | Description |
|----------|-------------|
| `critical` | Highest delivery priority. Still subject to credential and organization limits. |
| `high` | Processed before normal emails |
| `normal` | Standard priority (default) |
| `low` | Processed when queue is empty |

`POST /api/v1/send` requires a transactional credential with the explicit `send` scope. All priority values are rate-limited; `critical` cannot be used to bypass abuse controls.

If the per-minute organization or credential limit is exceeded before sending,
`POST /api/v1/send` and `POST /api/v1/wordpress` return HTTP `429` with
`details.code: RATE_LIMIT_EXCEEDED`, `details.resetAt`, a positive
`Retry-After` header and the same seconds in JSON `retryAfter`. No email is
persisted or submitted to SES for that attempt. This is separate from the
`409` idempotency-in-progress response below and from a failure after SES has
been contacted; do not blindly retry an ambiguous provider outcome.

If the organization's or credential's **daily** transactional quota, or the
organization's **monthly** quota, is used up, both endpoints return HTTP `429`
with `details.code: DAILY_QUOTA_EXCEEDED` or `MONTHLY_QUOTA_EXCEEDED`,
`details.quota` (`organization_daily`, `credential_daily` or
`organization_monthly`), `details.current`, `details.limit` and
`details.resetAt`. Daily quotas reset at the next midnight UTC, the monthly
quota on the first of the next month (UTC); `Retry-After` and JSON
`retryAfter` count the seconds until then. Nothing is persisted or sent.
Queue the mail for later or raise the limit instead of retrying right away.
(Before #410 these stops were HTTP `400` without a code.)

### Idempotent retries

Clients that may retry after a timeout should send an `Idempotency-Key` header
(1-255 visible ASCII characters without whitespace). The key is scoped to the
authenticated organization and retained for 24 hours; Mailaura stores only its
hash. An equivalent retry returns the original send resource/result without a
second SES call and includes `Idempotency-Replayed: true`.

- `409` + `details.code: IDEMPOTENCY_KEY_CONFLICT`: the key belongs to a
  different normalized payload.
- `409` + `details.code: IDEMPOTENCY_IN_PROGRESS` + `Retry-After: 1`: the first
  request is still active; retry the same payload/key.
- A provider response failure is a stable at-most-once result for that key.
  Use the returned `email_id`/email status endpoint to reconcile; do not create
  a new key merely because the client timed out.

The header is optional for backward compatibility. Without it, Mailaura cannot
deduplicate two independent HTTP requests. The same header is supported by the
WordPress send endpoint. See [send-idempotency.md](./send-idempotency.md).

### Example: Direct HTML

```bash
curl -X POST "https://www.mailaura.io/api/v1/send" \
  -H "X-Mailaura-Key: YOUR_API_KEY" \
  -H "X-Mailaura-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "subject": "Password Reset",
    "html": "<h1>Reset Your Password</h1><p>Click <a href=\"https://...\">here</a> to reset.</p>",
    "priority": "critical",
    "tags": ["password-reset"]
  }'
```

### Example: With Template

```bash
curl -X POST "https://www.mailaura.io/api/v1/send" \
  -H "X-Mailaura-Key: YOUR_API_KEY" \
  -H "X-Mailaura-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "template_slug": "order-confirmation",
    "template_variables": {
      "customer_name": "Max Mustermann",
      "order_id": "ORD-12345",
      "order_total": "€ 59,99"
    },
    "language": "de",
    "tags": ["order", "confirmation"],
    "metadata": {
      "order_id": "12345",
      "shop": "main-store"
    }
  }'
```

### Example: With PDF Attachment

```bash
# First, base64 encode your PDF
PDF_CONTENT=$(base64 -i invoice.pdf | tr -d '\n')

curl -X POST "https://www.mailaura.io/api/v1/send" \
  -H "X-Mailaura-Key: YOUR_API_KEY" \
  -H "X-Mailaura-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "from": "billing@mycompany.com",
    "subject": "Your Invoice INV-2024-00123",
    "html": "<p>Please find your invoice attached.</p>",
    "text": "Please find your invoice attached.",
    "attachments": [
      {
        "filename": "Invoice-2024-00123.pdf",
        "content": "'"$PDF_CONTENT"'",
        "contentType": "application/pdf"
      }
    ],
    "tags": ["invoice"],
    "metadata": {
      "invoice_id": "INV-2024-00123"
    }
  }'
```

### Example: Inline Image (Embedded)

```json
{
  "to": "customer@example.com",
  "subject": "Welcome!",
  "html": "<html><body><img src=\"cid:company-logo\" alt=\"Logo\"><p>Welcome!</p></body></html>",
  "attachments": [
    {
      "filename": "logo.png",
      "content": "iVBORw0KGgoAAAANSUhEUgAA...",
      "contentType": "image/png",
      "contentId": "company-logo",
      "disposition": "inline"
    }
  ]
}
```

### Success Response

```json
{
  "success": true,
  "data": {
    "message_id": "01234567-89ab-cdef-0123-456789abcdef",
    "email_id": "clxyz123abc456",
    "status": "sent",
    "sandbox": false
  },
  "timestamp": "2025-12-19T10:30:00.000Z"
}
```

### Sandbox Response

```json
{
  "success": true,
  "data": {
    "message_id": "sandbox_clxyz123abc456",
    "email_id": "clxyz123abc456",
    "status": "sandbox",
    "sandbox": true
  },
  "timestamp": "2025-12-19T10:30:00.000Z"
}
```

### Error Response

```json
{
  "error": "Missing required template variables",
  "details": {
    "missing": ["order_id", "customer_name"],
    "expected": ["order_id", "customer_name", "order_total"]
  },
  "timestamp": "2025-12-19T10:30:00.000Z"
}
```

---

## Email Delivery Status

Look up delivery status for transactional emails sent through any source (`/api/v1/send`, `/api/v1/wordpress`, WooCommerce integrations, etc.). Useful for AI agents and integrations that need to confirm a send actually happened or check whether a recipient bounced.

> **Authentication:** Supports **both** auth modes — Transactional Credentials (`X-Mailaura-Key` + `X-Mailaura-Secret`) **or** Standard API Key (`X-API-Key` / `Authorization: Bearer`). The `read` scope is required. Lookups are scoped to the calling organization — emails belonging to other orgs return `404` (not `403`) so existence is not leaked.
>
> Clients that send through `POST /api/v1/send` can poll status with the same Transactional Credentials they used to send.

### GET /emails/:id

Get a single transactional email by ID.

#### Path Parameters

| Parameter | Type   | Description                                       |
|-----------|--------|---------------------------------------------------|
| `id`      | string | The transactional email ID (cuid, e.g. `clx…`).   |

#### Response

```json
{
  "success": true,
  "data": {
    "id": "clxabc123def456",
    "to": "kontakt@example.com",
    "from": "noreply@yourdomain.com",
    "subject": "Welcome aboard",
    "status": "delivered",
    "priority": "normal",
    "message_id": "0100018a-…@eu-central-1.amazonses.com",
    "source": "wordpress",
    "tags": ["wp-system", "welcome"],
    "sent_at": "2026-05-07T10:23:10.000Z",
    "delivered_at": "2026-05-07T10:23:14.000Z",
    "opened_at": null,
    "clicked_at": null,
    "bounced_at": null,
    "complained_at": null,
    "error_code": null,
    "error_message": null,
    "retry_count": 0,
    "created_at": "2026-05-07T10:23:09.000Z",
    "updated_at": "2026-05-07T10:23:14.000Z"
  },
  "timestamp": "2026-05-07T10:23:20.000Z"
}
```

#### Status Values

`queued` · `sending` · `sent` · `delivered` · `opened` · `clicked` · `bounced` · `complained` · `failed` · `sandbox`

A typical successful flow: `queued → sending → sent → delivered → opened → clicked`. Bounces / complaints / failures terminate the flow; `error_code` and `error_message` carry the SES diagnostic.

#### Errors

| Status | Reason                                              |
|--------|-----------------------------------------------------|
| `401`  | Missing or invalid API key                          |
| `403`  | API key lacks the `read` scope                      |
| `404`  | Email not found in the calling org (or doesn't exist) |
| `429`  | Rate limit exceeded                                 |

### GET /emails

List recent transactional emails for the calling org, newest first.

#### Query Parameters

| Parameter | Type    | Description                                                                  |
|-----------|---------|------------------------------------------------------------------------------|
| `limit`   | integer | 1–200, default `50`.                                                         |
| `cursor`  | opaque  | Composite cursor encoding `(createdAt, id)`. Pass back the exact `next_cursor` value from the previous response — treat it as opaque. Stable under bursty sends that share a millisecond. |
| `status`  | enum    | One of the status values above. Unknown values are silently ignored.         |
| `source`  | string  | e.g. `wordpress`, `api`, `woocommerce`, `chatcmm`, `shopify`. Matches the serialized public label: a nonempty string `metadata._source` takes precedence exactly; without one, the column uses its historical one-suffix `-api` normalization. An explicit `wordpress-api` metadata label never matches `wordpress`. |
| `tag`     | string  | Matches if any tag in the email's `tags[]` equals this value.                |
| `to`      | email   | Exact recipient match.                                                       |
| `since`   | ISO ts  | Returns emails created at or **after** this timestamp.                       |

`source` is a public origin label, not verified transport provenance. Direct
`POST /api/v1/send` metadata `_source` nonempty strings (including custom
integrations) retain their existing public meaning; WordPress and SMTP origins
are route-owned. New rows store that label in the column and retain redundant
metadata during migration. Previously broad `api`, contradictory column-label and legacy literal-suffix
overmatches disappear when the corrected filter is deployed. Subsequent backfill
preserves source membership; metadata and legacy suffix fallback remain. See the
[compatibility and migration contract](transactional-source-migration.md).

`cursor` and `since` may be combined (window).

#### Response

```json
{
  "success": true,
  "data": {
    "emails": [
      { "id": "clx…", "to": "…", "status": "delivered", "...": "..." },
      { "id": "clx…", "to": "…", "status": "sent", "...": "..." }
    ],
    "next_cursor": "MjAyNi0wNS0wN1QxMDoyMjowMC4wMDBaX19jbHhhYmM="
  },
  "timestamp": "2026-05-07T10:23:20.000Z"
}
```

`next_cursor` is `null` on the last page. Treat the value as opaque — its encoding may evolve in future versions.

#### Pagination Example

```bash
# First page
curl -H "X-API-Key: $KEY" "https://www.mailaura.io/api/v1/emails?source=wordpress&limit=50"

# Next page — feed back the cursor verbatim
CURSOR="MjAyNi0wNS0wN1QxMDoyMjowMC4wMDBaX19jbHhhYmM="
curl -H "X-API-Key: $KEY" \
  "https://www.mailaura.io/api/v1/emails?source=wordpress&limit=50&cursor=$CURSOR"
```

The cursor is a stable composite key on `(createdAt, id)` — a paging client never re-reads the same row, and never silently drops rows when many sends share a single-millisecond timestamp.

---

## Transactional Lifecycle Webhooks

Subscribe external systems to push events for transactional email lifecycle changes. The payload mirrors `GET /api/v1/emails/:id` so integrations can use one parser for polling and webhooks.

> **Authentication:** Supports Transactional Credentials (`X-Mailaura-Key` + `X-Mailaura-Secret`) and Standard API Key (`X-API-Key` / `Authorization: Bearer`). `read` is required for `GET`. Transactional Credentials use the least-privilege `webhooks:write` scope for create/update/delete; existing Standard API Keys with `write` and wildcard keys remain compatible.

### Events

`EMAIL_SENT` · `EMAIL_DELIVERED` · `EMAIL_OPENED` · `EMAIL_CLICKED` · `EMAIL_BOUNCED` · `EMAIL_COMPLAINED` · `EMAIL_FAILED`

### Manage Subscriptions

```http
GET    /api/v1/transactional/webhooks
POST   /api/v1/transactional/webhooks
PATCH  /api/v1/transactional/webhooks/:id
DELETE /api/v1/transactional/webhooks/:id
```

#### Create

```json
{
  "name": "CRM lifecycle events",
  "url": "https://crm.example.com/webhooks/mailaura",
  "events": ["EMAIL_DELIVERED", "EMAIL_BOUNCED", "EMAIL_COMPLAINED"]
}
```

Response includes the signing secret only once:

```json
{
  "success": true,
  "data": {
    "webhook": {
      "id": "clxwh123",
      "name": "CRM lifecycle events",
      "url": "https://crm.example.com/webhooks/mailaura",
      "events": ["EMAIL_DELIVERED", "EMAIL_BOUNCED", "EMAIL_COMPLAINED"],
      "is_active": true,
      "last_triggered_at": null,
      "last_success_at": null,
      "last_failure_at": null,
      "failure_count": 0,
      "success_count": 0,
      "created_at": "2026-05-19T09:10:00.000Z",
      "updated_at": "2026-05-19T09:10:00.000Z"
    },
    "secret": "whsec_..."
  }
}
```

#### Update / Unsubscribe

`PATCH` accepts `name`, `url`, `events`, and `is_active`. Use `DELETE /api/v1/transactional/webhooks/:id` to unsubscribe completely.

### Delivery Payload

```json
{
  "event": "EMAIL_DELIVERED",
  "event_at": "2026-05-19T09:15:01.000Z",
  "timestamp": "2026-05-19T09:15:02.000Z",
  "data": {
    "id": "clxemail123",
    "emailId": "clxemail123",
    "to": "user@example.com",
    "from": "noreply@example.com",
    "subject": "Welcome aboard",
    "status": "delivered",
    "priority": "normal",
    "message_id": "0100018a-...",
    "messageId": "0100018a-...",
    "source": "api",
    "tags": ["welcome"],
    "sent_at": "2026-05-19T09:14:58.000Z",
    "delivered_at": "2026-05-19T09:15:01.000Z",
    "opened_at": null,
    "clicked_at": null,
    "bounced_at": null,
    "complained_at": null,
    "error_code": null,
    "error_message": null,
    "retry_count": 0,
    "created_at": "2026-05-19T09:14:57.000Z",
    "updated_at": "2026-05-19T09:15:01.000Z",
    "event_details": {
      "recipients": ["user@example.com"],
      "processing_time_millis": 1200
    }
  }
}
```

### Signature

Every request includes:

```http
X-Mailaura-Signature: <hex hmac sha256>
X-Mailaura-Timestamp: <unix seconds>
X-Mailaura-Event: EMAIL_DELIVERED
```

Compute `HMAC-SHA256(raw_request_body, webhook_secret)` and compare it to `X-Mailaura-Signature`. Reject timestamps older than 5 minutes to reduce replay risk.

Failed deliveries retry with exponential backoff and are tracked in the dashboard delivery history.

---

## Transactional Templates

Manage transactional email templates programmatically. Templates support multiple languages and are referenced by `slug` when sending emails.

> **Authentication:** Supports both Transactional Credentials (`X-Mailaura-Key`/`X-Mailaura-Secret`) and Standard API Keys (`X-API-Key`).

---

### GET /transactional/templates

List all transactional templates with pagination.

```http
GET /api/v1/transactional/templates
```

#### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | integer | 1 | Page number |
| `limit` | integer | 50 | Items per page (max: 100) |
| `language` | string | - | Filter by language code (e.g., "de", "en") |
| `search` | string | - | Search in name and slug |
| `active` | boolean | - | Filter by active status |

#### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clx123abc",
      "name": "Password Reset",
      "slug": "password-reset",
      "language": "de",
      "subject": "Passwort zurücksetzen",
      "description": "Sent when user requests password reset",
      "variables": ["user_name", "reset_link", "expiry_time"],
      "isActive": true,
      "isDefault": false,
      "stats": {
        "sent": 1234,
        "opened": 456,
        "clicked": 89
      },
      "createdAt": "2025-01-15T10:00:00.000Z",
      "updatedAt": "2025-12-20T14:30:00.000Z"
    }
  ],
  "pagination": {
    "total": 25,
    "page": 1,
    "limit": 50,
    "totalPages": 1,
    "hasMore": false
  }
}
```

---

### POST /transactional/templates

Create a new transactional template.

```http
POST /api/v1/transactional/templates
```

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Template name (max 200 chars) |
| `slug` | string | ✅ | Unique identifier (lowercase, hyphens only) |
| `language` | string | ❌ | ISO language code (default: "de") |
| `subject` | string | ✅ | Email subject line (max 500 chars) |
| `html` | string | ✅ | HTML content |
| `text` | string | ❌ | Plain text content |
| `variables` | array | ❌ | Expected variable names (for validation) |
| `description` | string | ❌ | Template description (max 500 chars) |
| `isActive` | boolean | ❌ | Active status (default: true) |

#### Request Example

```json
{
  "name": "Password Reset",
  "slug": "password-reset",
  "language": "de",
  "subject": "Passwort zurücksetzen für {{user_name}}",
  "html": "<!DOCTYPE html><html><body><h1>Hallo {{user_name}}</h1><p>Klicken Sie <a href=\"{{reset_link}}\">hier</a> zum Zurücksetzen.</p></body></html>",
  "text": "Hallo {{user_name}}, klicken Sie hier zum Zurücksetzen: {{reset_link}}",
  "variables": ["user_name", "reset_link", "expiry_time"],
  "description": "Sent when user requests password reset"
}
```

#### Response

```json
{
  "success": true,
  "data": {
    "id": "clx456def",
    "name": "Password Reset",
    "slug": "password-reset",
    "language": "de",
    "subject": "Passwort zurücksetzen für {{user_name}}",
    "description": "Sent when user requests password reset",
    "variables": ["user_name", "reset_link", "expiry_time"],
    "isActive": true,
    "createdAt": "2025-12-23T10:00:00.000Z",
    "updatedAt": "2025-12-23T10:00:00.000Z"
  },
  "timestamp": "2025-12-23T10:00:00.000Z"
}
```

#### Errors

- `409` - Template with this slug and language already exists

---

### GET /transactional/templates/:slug

Get template(s) by slug. Returns single template if language is specified, or all language versions if not.

```http
GET /api/v1/transactional/templates/:slug
GET /api/v1/transactional/templates/:slug?language=de
```

#### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `language` | string | ISO language code. If omitted, returns all languages. |

#### Response (Single Language)

```json
{
  "success": true,
  "data": {
    "id": "clx123abc",
    "name": "Password Reset",
    "slug": "password-reset",
    "language": "de",
    "subject": "Passwort zurücksetzen für {{user_name}}",
    "html": "<!DOCTYPE html>...",
    "text": "...",
    "description": "Sent when user requests password reset",
    "variables": ["user_name", "reset_link", "expiry_time"],
    "isActive": true,
    "isDefault": false,
    "stats": {
      "sent": 1234,
      "opened": 456,
      "clicked": 89
    },
    "createdAt": "2025-01-15T10:00:00.000Z",
    "updatedAt": "2025-12-20T14:30:00.000Z"
  }
}
```

#### Response (All Languages)

```json
{
  "success": true,
  "data": {
    "slug": "password-reset",
    "languages": ["de", "en", "fr"],
    "templates": [
      {
        "id": "clx123abc",
        "language": "de",
        "subject": "Passwort zurücksetzen",
        ...
      },
      {
        "id": "clx456def",
        "language": "en",
        "subject": "Reset Your Password",
        ...
      }
    ]
  }
}
```

---

### PUT /transactional/templates/:slug

Update an existing template. Language is required as query parameter.

```http
PUT /api/v1/transactional/templates/:slug?language=de
```

#### Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `language` | string | ✅ | ISO language code of template to update |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ❌ | Template name |
| `subject` | string | ❌ | Email subject line |
| `html` | string | ❌ | HTML content |
| `text` | string | ❌ | Plain text content (null to remove) |
| `variables` | array | ❌ | Expected variable names |
| `description` | string | ❌ | Template description (null to remove) |
| `isActive` | boolean | ❌ | Active status |

#### Request Example

```json
{
  "subject": "Passwort zurücksetzen - Gültig für {{expiry_time}}",
  "isActive": true
}
```

#### Response

```json
{
  "success": true,
  "data": {
    "id": "clx123abc",
    "name": "Password Reset",
    "slug": "password-reset",
    "language": "de",
    "subject": "Passwort zurücksetzen - Gültig für {{expiry_time}}",
    "description": "Sent when user requests password reset",
    "variables": ["user_name", "reset_link", "expiry_time"],
    "isActive": true,
    "createdAt": "2025-01-15T10:00:00.000Z",
    "updatedAt": "2025-12-23T11:30:00.000Z"
  }
}
```

---

### DELETE /transactional/templates/:slug

Delete a template. If language is specified, deletes only that version. Otherwise, deletes ALL language versions.

```http
DELETE /api/v1/transactional/templates/:slug?language=de
DELETE /api/v1/transactional/templates/:slug
```

#### Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `language` | string | ❌ | ISO language code. If omitted, deletes ALL versions. |

#### Response (Single Language)

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "slug": "password-reset",
    "language": "de"
  }
}
```

#### Response (All Languages)

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "slug": "password-reset",
    "deletedCount": 3,
    "languages": ["de", "en", "fr"]
  }
}
```

#### Errors

- `400` - Cannot delete default system template
- `400` - Template has been used by X email(s) (consider deactivating instead)
- `404` - Template not found

---

# Campaigns

## List Campaigns

Retrieve a paginated list of campaigns.

```http
GET /api/v1/campaigns
```

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `limit` | integer | Items per page (default: 50, max: 100) |
| `status` | string | Filter by status: `draft`, `scheduled`, `sending`, `sent`, `cancelled` |
| `listId` | string | Filter by list ID |
| `search` | string | Search in name and subject |

### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clx123abc",
      "name": "Welcome Campaign",
      "subject": "Welcome to our newsletter!",
      "preheader": "We're excited to have you",
      "status": "sent",
      "list": {
        "id": "clx456def",
        "name": "Newsletter"
      },
      "scheduledAt": null,
      "sentAt": "2024-01-15T10:30:00.000Z",
      "stats": {
        "recipients": 1500,
        "sent": 1500,
        "opened": 450,
        "clicked": 120,
        "bounced": 5,
        "unsubscribed": 3
      },
      "createdAt": "2024-01-10T08:00:00.000Z",
      "updatedAt": "2024-01-15T10:30:00.000Z"
    }
  ],
  "pagination": {
    "total": 25,
    "page": 1,
    "limit": 50,
    "totalPages": 1,
    "hasMore": false
  }
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/campaigns?status=sent&limit=10" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Get Campaign

Retrieve a single campaign by ID with detailed statistics.

```http
GET /api/v1/campaigns/:id
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx123abc",
    "name": "Welcome Campaign",
    "subject": "Welcome to our newsletter!",
    "preheader": "We're excited to have you",
    "html": "<html>...</html>",
    "status": "sent",
    "scheduledAt": null,
    "sentAt": "2024-01-15T10:30:00.000Z",
    "list": {
      "id": "clx456def",
      "name": "Newsletter"
    },
    "template": {
      "id": "clx789ghi",
      "name": "Default Template"
    },
    "stats": {
      "recipients": 1500,
      "sent": 1500,
      "delivered": 1490,
      "opened": 450,
      "clicked": 120,
      "bounced": 5,
      "complained": 1,
      "unsubscribed": 3
    },
    "rates": {
      "openRate": "30.00",
      "clickRate": "26.67",
      "bounceRate": "0.33",
      "unsubscribeRate": "0.20"
    },
    "createdAt": "2024-01-10T08:00:00.000Z",
    "updatedAt": "2024-01-15T10:30:00.000Z"
  }
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/campaigns/clx123abc" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Create Campaign

Create a new campaign.

```http
POST /api/v1/campaigns
```

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✓ | Campaign name (max 200 chars) |
| `subject` | string | ✓ | Email subject (max 500 chars) |
| `preheader` | string | | Preview text (max 200 chars) |
| `listId` | string or null | | Optional recipient list ID; required for scheduling or sending |
| `html` | string | ✓ | Email HTML content |
| `templateId` | string | | Template ID to use |
| `scheduledAt` | ISO 8601 | | Schedule send time |

### Request Example

```json
{
  "name": "Spring Sale Announcement",
  "subject": "🌸 Spring Sale - 30% off everything!",
  "preheader": "Don't miss our biggest sale of the year",
  "listId": "clx456def",
  "html": "<!DOCTYPE html><html><body><h1>Spring Sale!</h1><p>30% off all items this week.</p></body></html>",
  "scheduledAt": "2024-03-21T09:00:00.000Z"
}
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx789xyz",
    "name": "Spring Sale Announcement",
    "subject": "🌸 Spring Sale - 30% off everything!",
    "preheader": "Don't miss our biggest sale of the year",
    "status": "scheduled",
    "listId": "clx456def",
    "scheduledAt": "2024-03-21T09:00:00.000Z",
    "createdAt": "2024-03-15T14:00:00.000Z",
    "updatedAt": "2024-03-15T14:00:00.000Z"
  }
}
```

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/campaigns" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Spring Sale Announcement",
    "subject": "🌸 Spring Sale - 30% off everything!",
    "listId": "clx456def",
    "html": "<html><body><h1>Sale!</h1></body></html>"
  }'
```

---

## Update Campaign

Update an existing campaign. Only draft or scheduled campaigns can be updated.

```http
PUT /api/v1/campaigns/:id
```

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | | Campaign name (max 200 chars) |
| `subject` | string | | Email subject (max 500 chars) |
| `preheader` | string | | Preview text (max 200 chars, null to remove) |
| `html` | string | | Email HTML content |
| `listId` | string or null | | Recipient list ID; null detaches the audience and cancels scheduling |
| `scheduledAt` | ISO 8601 | | Schedule send time (null to unschedule) |

### Request Example

```json
{
  "subject": "🌸 Spring Sale - Now 40% off!",
  "scheduledAt": null
}
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx789xyz",
    "name": "Spring Sale Announcement",
    "subject": "🌸 Spring Sale - Now 40% off!",
    "preheader": "Don't miss our biggest sale of the year",
    "status": "draft",
    "scheduledAt": null,
    "list": {
      "id": "clx456def",
      "name": "Newsletter"
    },
    "createdAt": "2024-03-15T14:00:00.000Z",
    "updatedAt": "2024-03-16T10:00:00.000Z"
  }
}
```

### Errors

- `400` - Cannot update a campaign that has been sent or is sending
- `404` - Campaign not found

---

## Delete Campaign

Delete a campaign. Only draft or scheduled campaigns can be deleted.

```http
DELETE /api/v1/campaigns/:id
```

### Response

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "id": "clx789xyz"
  }
}
```

### Errors

- `400` - Cannot delete a campaign that has been sent
- `404` - Campaign not found

---

## Send Campaign

Send a campaign immediately.

```http
POST /api/v1/campaigns/:id/send
```

**Required Scope:** `send`

The request validates the stored segment and double-opt-in eligibility, applies
the organization trust and billing gates, and queues the campaign for
asynchronous delivery. HTTP `202` means the queue accepted the job; it does not
mean every recipient has already been delivered.

`Idempotency-Key` is supported and follows the same 24-hour conflict/replay
contract as transactional sends. If omitted, Mailaura safely derives the
logical key from the organization-scoped campaign ID. A retry after confirmed
queue acceptance returns the original `jobId`; an unconfirmed queue handoff is
submitted again only with that same Inngest event ID.

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx789xyz",
    "status": "sending",
    "jobId": "campaign-clx789xyz-550e8400-e29b-41d4-a716-446655440000",
    "recipientCount": 1500,
    "estimatedDuration": 1525,
    "queuedAt": "2026-08-20T10:30:00.000Z"
  }
}
```

### Errors

- `400` - Cannot send a campaign with status '{status}'
- `400` - No eligible subscribers in selected list / no subscribers match the stored segment
- `400` - Campaign is missing required fields (subject, html)
- `403` - Insufficient permissions - requires 'send' scope
- `403` - Organization trust or billing status blocks marketing sends
- `404` - Campaign not found
- `409` - Another request already claimed the campaign for sending
- `409` - Idempotency key is still processing or belongs to another campaign
- `503` - Queue acceptance could not be confirmed. Retry the same request; Mailaura reuses the stable job/event ID.

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/campaigns/clx789xyz/send" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Idempotency-Key: launch-campaign-2026-08-21"
```

---

## Get Campaign Statistics

Retrieve detailed statistics for a campaign.

```http
GET /api/v1/campaigns/:id/stats
```

### Response

```json
{
  "success": true,
  "data": {
    "campaignId": "clx789xyz",
    "campaignName": "Spring Sale Announcement",
    "status": "sent",
    "sentAt": "2024-03-16T10:30:00.000Z",
    "stats": {
      "sent": 1500,
      "delivered": 1490,
      "opened": 450,
      "clicked": 120,
      "bounced": 5,
      "complained": 1,
      "unsubscribed": 3,
      "uniqueOpens": 380,
      "uniqueClicks": 95
    },
    "rates": {
      "openRate": "30.20",
      "uniqueOpenRate": "25.50",
      "clickRate": "26.67",
      "uniqueClickRate": "25.00",
      "clickToOpenRate": "26.67",
      "bounceRate": "0.33",
      "unsubscribeRate": "0.20",
      "complaintRate": "0.07"
    },
    "topLinks": [
      {
        "url": "https://example.com/sale",
        "clicks": 85
      },
      {
        "url": "https://example.com/products",
        "clicks": 35
      }
    ],
    "opensOverTime": [
      {
        "date": "2024-03-16T11:00:00.000Z",
        "count": 150
      },
      {
        "date": "2024-03-16T12:00:00.000Z",
        "count": 120
      }
    ]
  }
}
```

---

## Send Test Email

Send a test email for a campaign to a specific email address.

```http
POST /api/v1/campaigns/:id/test
```

**Scope:** `send`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | string | ✓ | Email address to send test to |

### Response

```json
{
  "success": true,
  "data": {
    "sent": true,
    "email": "test@example.com"
  }
}
```

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/campaigns/clx789xyz/test" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com"
  }'
```

---

## Duplicate Campaign

Create a copy of an existing campaign as a new draft.

```http
POST /api/v1/campaigns/:id/duplicate
```

**Scope:** `write`

### Response (201)

```json
{
  "success": true,
  "data": {
    "id": "clx999new",
    "name": "Spring Sale Announcement (Copy)",
    "subject": "🌸 Spring Sale - 30% off everything!",
    "status": "draft",
    "createdAt": "2024-03-16T15:00:00.000Z",
    "updatedAt": "2024-03-16T15:00:00.000Z"
  }
}
```

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/campaigns/clx789xyz/duplicate" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Pause Campaign

Pause a campaign that is currently sending.

```http
POST /api/v1/campaigns/:id/pause
```

**Scope:** `write`

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx789xyz",
    "status": "paused"
  }
}
```

### Errors

- `400` - Campaign is not currently sending
- `400` - Letter campaign (letter campaigns pause automatically when the letter balance runs out)
- `404` - Campaign not found

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/campaigns/clx789xyz/pause" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Resume Campaign

Resume a paused campaign.

```http
POST /api/v1/campaigns/:id/resume
```

**Scope:** `write`

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx789xyz",
    "status": "sending"
  }
}
```

### Errors

- `400` - Campaign is not paused
- `400` - Letter campaign (resume letter campaigns from the dashboard, which checks the letter balance)
- `404` - Campaign not found

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/campaigns/clx789xyz/resume" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

# Tags

Tags are labels stored as string arrays on subscribers. They are not separate entities — they exist only on subscribers.

## List Tags

Retrieve all unique tags across the organization with subscriber counts.

```http
GET /api/v1/tags
```

**Scope:** `read`

### Response

```json
{
  "success": true,
  "data": [
    { "name": "vip", "count": 145 },
    { "name": "newsletter", "count": 892 }
  ]
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/tags" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Create/Assign Tag

Create a tag and assign it to subscribers.

```http
POST /api/v1/tags
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `tag` | string | ✓ | Tag name (max 50 chars) |
| `subscriberIds` | array | ✓ | Subscriber IDs to tag |

### Response (201)

```json
{
  "success": true,
  "data": {
    "tag": "vip",
    "updated": 5
  }
}
```

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/tags" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "tag": "vip",
    "subscriberIds": ["clxsub001", "clxsub002", "clxsub003"]
  }'
```

---

## Rename Tag

Rename a tag across all subscribers.

```http
PUT /api/v1/tags/:name
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `newName` | string | ✓ | New tag name |

### Response

```json
{
  "success": true,
  "data": {
    "oldName": "vip",
    "newName": "premium",
    "updated": 145
  }
}
```

### Example

```bash
curl -X PUT "https://www.mailaura.io/api/v1/tags/vip" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "newName": "premium"
  }'
```

---

## Delete Tag

Remove a tag from all subscribers.

```http
DELETE /api/v1/tags/:name
```

**Scope:** `write`

### Response

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "tag": "vip",
    "subscribersAffected": 145
  }
}
```

### Example

```bash
curl -X DELETE "https://www.mailaura.io/api/v1/tags/vip" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Assign Tag to Subscribers

Assign an existing tag to additional subscribers.

```http
POST /api/v1/tags/:name/assign
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `subscriberIds` | array | ✓ | Subscriber IDs |

### Response

```json
{
  "success": true,
  "data": {
    "tag": "vip",
    "assigned": 5
  }
}
```

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/tags/vip/assign" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriberIds": ["clxsub004", "clxsub005"]
  }'
```

---

## Remove Tag from Subscribers

Remove a tag from specific subscribers.

```http
POST /api/v1/tags/:name/remove
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `subscriberIds` | array | ✓ | Subscriber IDs |

### Response

```json
{
  "success": true,
  "data": {
    "tag": "vip",
    "removed": 3
  }
}
```

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/tags/vip/remove" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriberIds": ["clxsub001", "clxsub002", "clxsub003"]
  }'
```

---

# Media

Manage uploaded media files (images, documents) stored in S3.

## List Media

Retrieve a paginated list of media files.

```http
GET /api/v1/media
```

**Scope:** `read`

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `limit` | integer | Items per page (default: 50, max: 100) |
| `search` | string | Search by filename, title, or alt text |

### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clxmedia001",
      "filename": "hero-image.jpg",
      "url": "https://cdn.mailaura.io/media/hero-image.jpg",
      "contentType": "image/jpeg",
      "size": 245000,
      "alt": "Hero banner image",
      "title": "Hero Image",
      "folder": null,
      "usageCount": 3,
      "createdAt": "2024-02-15T10:00:00.000Z",
      "updatedAt": "2024-03-10T14:00:00.000Z"
    }
  ],
  "pagination": {
    "total": 85,
    "page": 1,
    "limit": 50,
    "totalPages": 2,
    "hasMore": true
  }
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/media?search=hero&limit=10" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Get Media

Retrieve a single media file by ID with usage information.

```http
GET /api/v1/media/:id
```

**Scope:** `read`

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxmedia001",
    "filename": "hero-image.jpg",
    "url": "https://cdn.mailaura.io/media/hero-image.jpg",
    "contentType": "image/jpeg",
    "size": 245000,
    "alt": "Hero banner image",
    "title": "Hero Image",
    "folder": null,
    "usageCount": 3,
    "createdAt": "2024-02-15T10:00:00.000Z",
    "updatedAt": "2024-03-10T14:00:00.000Z"
  }
}
```

---

## Update Media

Update metadata for a media file.

```http
PUT /api/v1/media/:id
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `alt` | string | | Alt text (max 500 chars) |
| `title` | string | | Display title (max 200 chars) |

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxmedia001",
    "filename": "hero-image.jpg",
    "alt": "Updated hero banner",
    "title": "Spring Campaign Hero",
    "updatedAt": "2024-03-16T15:00:00.000Z"
  }
}
```

### Example

```bash
curl -X PUT "https://www.mailaura.io/api/v1/media/clxmedia001" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "alt": "Updated hero banner",
    "title": "Spring Campaign Hero"
  }'
```

---

## Delete Media

Delete a media file from S3 and the database.

```http
DELETE /api/v1/media/:id
```

**Scope:** `write`

### Response

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "id": "clxmedia001"
  }
}
```

### Example

```bash
curl -X DELETE "https://www.mailaura.io/api/v1/media/clxmedia001" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Get Storage Quota

Returns storage usage and limits based on the current plan.

```http
GET /api/v1/media/quota
```

**Scope:** `read`

### Response

```json
{
  "success": true,
  "data": {
    "used": 52428800,
    "limit": 1073741824,
    "usedFormatted": "50 MB",
    "limitFormatted": "1 GB",
    "percentage": 4.88
  }
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/media/quota" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

# Brand

Manage organization brand settings for email templates and campaigns.

## Update Brand Settings

Update brand configuration for the organization.

```http
PUT /api/v1/brand
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `organizationName` | string | | Organization name |
| `primaryColor` | string | | Hex color (e.g., `#10b981`) |
| `secondaryColor` | string | | Hex color (nullable) |
| `logoUrl` | string | | Logo URL (nullable) |
| `website` | string | | Website URL (nullable) |

### Response

```json
{
  "success": true,
  "data": {
    "organizationName": "Acme GmbH",
    "primaryColor": "#10b981",
    "secondaryColor": "#064e3b",
    "logoUrl": "https://cdn.mailaura.io/media/acme-logo.png",
    "website": "https://acme.example.com",
    "updatedAt": "2024-03-16T15:00:00.000Z"
  }
}
```

### Example

```bash
curl -X PUT "https://www.mailaura.io/api/v1/brand" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "primaryColor": "#10b981",
    "secondaryColor": "#064e3b",
    "website": "https://acme.example.com"
  }'
```

---

## Analyze Brand

Analyze a website to extract brand colors and fonts. **Pro plan required.**

```http
POST /api/v1/brand/analyze
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | ✓ | Website URL to analyze |

### Response

```json
{
  "success": true,
  "data": {
    "url": "https://acme.example.com",
    "colors": {
      "primary": "#10b981",
      "secondary": "#064e3b",
      "accent": "#f59e0b"
    },
    "fonts": ["Inter", "Georgia"],
    "logoUrl": "https://acme.example.com/logo.png"
  }
}
```

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/brand/analyze" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://acme.example.com"
  }'
```

---

# Analytics

## Analytics Overview

Retrieve account-wide analytics.

```http
GET /api/v1/analytics/overview
```

**Scope:** `read`

### Response

```json
{
  "success": true,
  "data": {
    "subscribers": {
      "total": 12500,
      "active": 11800
    },
    "lists": 8,
    "campaigns": {
      "total": 45,
      "sent": 38
    },
    "performance": {
      "totalSent": 285000,
      "totalOpened": 85500,
      "totalClicked": 22800,
      "totalBounced": 1425,
      "avgOpenRate": "30.00",
      "avgClickRate": "8.00",
      "avgBounceRate": "0.50"
    }
  }
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/analytics/overview" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Engagement Trends

Retrieve daily engagement breakdown over a period.

```http
GET /api/v1/analytics/engagement
```

**Scope:** `read`

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `days` | integer | Lookback period (default: 30, max: 365) |
| `campaignId` | string | Filter to specific campaign |

### Response

```json
{
  "success": true,
  "data": [
    {
      "date": "2024-03-15",
      "opens": 320,
      "clicks": 85,
      "bounces": 3,
      "unsubscribes": 1
    },
    {
      "date": "2024-03-14",
      "opens": 280,
      "clicks": 72,
      "bounces": 2,
      "unsubscribes": 0
    }
  ]
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/analytics/engagement?days=7" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Device Stats

Retrieve device and browser breakdown for opens and clicks.

```http
GET /api/v1/analytics/devices
```

**Scope:** `read`

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `days` | integer | Lookback period (default: 30) |
| `campaignId` | string | Filter to specific campaign |

### Response

```json
{
  "success": true,
  "data": {
    "devices": [
      { "name": "Mobile", "percentage": 58.2, "count": 4950 },
      { "name": "Desktop", "percentage": 35.1, "count": 2985 },
      { "name": "Tablet", "percentage": 6.7, "count": 570 }
    ],
    "browsers": [
      { "name": "Apple Mail", "percentage": 42.0, "count": 3570 },
      { "name": "Gmail", "percentage": 28.5, "count": 2423 },
      { "name": "Outlook", "percentage": 18.3, "count": 1556 }
    ]
  }
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/analytics/devices?days=30" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

# Profile

## Get Profile

Retrieve organization profile settings.

```http
GET /api/v1/profile
```

**Scope:** `read`

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxorg001",
    "name": "Acme GmbH",
    "slug": "acme-gmbh",
    "website": "https://acme.example.com",
    "logo": "https://cdn.mailaura.io/media/acme-logo.png",
    "senderName": "Acme Newsletter",
    "senderEmail": "newsletter@acme.example.com",
    "companyAddress": "Musterstraße 1, 1010 Wien, Austria",
    "companyDescription": "Leading provider of innovative solutions",
    "accentColor": "#10b981",
    "secondaryColor": "#064e3b",
    "plan": "pro",
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-03-15T08:00:00.000Z"
  }
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/profile" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Update Profile

Update organization profile settings.

```http
PUT /api/v1/profile
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | | Organization name |
| `website` | string | | Website URL |
| `senderName` | string | | Default sender name |
| `senderEmail` | string | | Default sender email |
| `companyAddress` | string | | Physical address |
| `companyDescription` | string | | Company description |

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxorg001",
    "name": "Acme GmbH",
    "slug": "acme-gmbh",
    "website": "https://acme.example.com",
    "senderName": "Acme Newsletter",
    "senderEmail": "newsletter@acme.example.com",
    "companyAddress": "Neue Straße 5, 1020 Wien, Austria",
    "companyDescription": "Leading provider of innovative solutions",
    "updatedAt": "2024-03-16T15:00:00.000Z"
  }
}
```

### Example

```bash
curl -X PUT "https://www.mailaura.io/api/v1/profile" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "senderName": "Acme Newsletter",
    "companyAddress": "Neue Straße 5, 1020 Wien, Austria"
  }'
```

---

# API Keys

## List API Keys

Retrieve all API keys for the authenticated organization, including `expiresAt` and `lastUsedAt`. The list never includes full keys or hashes. Expired keys remain visible for review but cannot authenticate requests.

```http
GET /api/v1/api-keys
```

**Scope:** `read`

### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clxkey001",
      "name": "Production Key",
      "keyPreview": "ml_live_abc...xyz",
      "scopes": ["read", "write", "send"],
      "lastUsedAt": "2024-03-16T10:30:00.000Z",
      "expiresAt": null,
      "createdAt": "2024-01-15T09:00:00.000Z"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/api-keys" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Create API Key

Create a new API key for the authenticated organization. A credential can delegate only scopes it already has (exact scope or `*`) and cannot delegate a longer lifetime. `write` alone does not authorize granting `read`, `send`, `*` or resource-specific scopes. A wildcard caller still has a lifetime ceiling.

```http
POST /api/v1/api-keys
```

**Scope:** `write`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✓ | Key name |
| `scopes` | array | | Permission scopes (default: `["read", "write"]`) |
| `expiresAt` | string or null | | Future ISO 8601 timestamp with `Z` or a numeric timezone offset; returned as UTC. Omission inherits the caller's finite expiry, otherwise no expiry. Explicit `null` means no expiry and is allowed only when the caller has no expiry. |

Invalid, timezone-free, past or current expiry timestamps return `400`. Scopes not covered by the caller, or an explicit unlimited/later expiry for an expiring caller, return `403` before key creation or quota lookup. For a `write`-only caller, the default `["read", "write"]` would exceed its scopes; explicitly request `scopes: ["write"]` instead. Existing unexpired `read` + `write` callers without expiry retain the default behavior. Plan access, suspension, rate limiting and API-key quota still apply.

### Response (201)

```json
{
  "success": true,
  "data": {
    "id": "clxkey002",
    "name": "Integration Key",
    "key": "ml_live_newkey123abc456def789ghi",
    "keyPreview": "ml_live_new...ghi",
    "scopes": ["read", "write"],
    "expiresAt": null,
    "createdAt": "2024-03-16T15:00:00.000Z"
  }
}
```

> **Note:** The raw `key` value is only returned once at creation time. Store it securely.

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/api-keys" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Integration Key",
    "scopes": ["read", "write"]
  }'
```

---

## Delete API Key

Delete an API key.

```http
DELETE /api/v1/api-keys/:id
```

**Scope:** `write`

### Response

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "id": "clxkey002"
  }
}
```

### Example

```bash
curl -X DELETE "https://www.mailaura.io/api/v1/api-keys/clxkey002" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

## Replace an Integration Key

REST uses the existing create-new-then-delete workflow; there is no REST regeneration endpoint or atomic rotation operation.

1. With a valid management key containing `write` and every requested scope, create a replacement in the same organization. Choose only the integration's required scopes and a future `expiresAt` within the management key's lifetime. Omit expiry to inherit a finite management-key expiry. A spare API-key quota slot is required; if the limit is reached, keep the old integration key active until a slot is available.
2. Store the creation response's one-time `key` securely. The list exposes only `keyPreview`, `expiresAt` and usage metadata; it cannot recover the secret.
3. Update all intended integration consumers and verify their required operations using the new key before deleting the old one. Keep the old key available during this transition. A failed or ambiguous create response cannot retrieve the secret later; reconcile key metadata before repeating creation, because this endpoint has no idempotency guarantee.
4. Delete the old key by its ID with a still-valid `write` credential in the same organization. Future requests using the old key then return `401`. Review the replacement's `lastUsedAt` and `expiresAt` through GET using a credential with `read`.

The dashboard/tRPC regeneration flow updates the existing key's hash and preview in place, resets `lastUsedAt`, retains its scopes and expiry, and invalidates the old secret immediately. REST replacement deliberately uses a new record and an overlap period, so verify consumers before deletion. Neither workflow changes scopes or extends the parent credential's authority through REST. This documentation does not perform a real key creation, rotation or deletion.

---

# Lists

## List All Lists

Retrieve a paginated list of subscriber lists.

```http
GET /api/v1/lists
```

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `limit` | integer | Items per page (default: 50, max: 100) |
| `search` | string | Search in name and description |

### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clx456def",
      "name": "Newsletter",
      "description": "Main newsletter subscribers",
      "doubleOptIn": true,
      "subscriberCount": 5420,
      "createdAt": "2024-01-01T00:00:00.000Z",
      "updatedAt": "2024-03-15T08:00:00.000Z"
    }
  ],
  "pagination": {
    "total": 3,
    "page": 1,
    "limit": 50,
    "totalPages": 1,
    "hasMore": false
  }
}
```

---

## Get List

Retrieve a single list by ID with subscriber statistics.

```http
GET /api/v1/lists/:id
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx456def",
    "name": "Newsletter",
    "description": "Main newsletter subscribers",
    "doubleOptIn": true,
    "stats": {
      "total": 5420,
      "active": 5100,
      "pending": 200,
      "unsubscribed": 120
    },
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-03-15T08:00:00.000Z"
  }
}
```

---

## Create List

Create a new subscriber list.

```http
POST /api/v1/lists
```

### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `name` | string | ✓ | | List name (max 100 chars) |
| `description` | string | | | List description (max 500 chars) |
| `doubleOptIn` | boolean | | true | Require email confirmation |

### Request Example

```json
{
  "name": "Product Updates",
  "description": "Subscribers interested in product news",
  "doubleOptIn": true
}
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx999abc",
    "name": "Product Updates",
    "description": "Subscribers interested in product news",
    "doubleOptIn": true,
    "subscriberCount": 0,
    "createdAt": "2024-03-16T12:00:00.000Z",
    "updatedAt": "2024-03-16T12:00:00.000Z"
  }
}
```

### Errors

- `409` - List with this name already exists

---

## Update List

Update an existing list.

```http
PUT /api/v1/lists/:id
```

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | | List name (max 100 chars) |
| `description` | string | | List description (max 500 chars, null to remove) |
| `doubleOptIn` | boolean | | Require email confirmation |

### Response

```json
{
  "success": true,
  "data": {
    "id": "clx456def",
    "name": "Newsletter (Updated)",
    "description": "Main newsletter subscribers",
    "doubleOptIn": true,
    "subscriberCount": 5420,
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-03-16T12:30:00.000Z"
  }
}
```

---

## Delete List

Delete a list and all associated subscribers. Campaigns, RSS campaign definitions, content and campaign history are retained. Their list references become null; scheduled campaigns return to draft, running campaigns and RSS campaigns pause. Reassign a list before explicitly scheduling or sending again.

```http
DELETE /api/v1/lists/:id
```

### Response

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "id": "clx456def",
    "subscribersRemoved": 5420
  }
}
```

**⚠️ Warning:** This action is irreversible. All subscribers in the list will be permanently deleted.

---

# Subscribers

## List Subscribers

Retrieve a paginated list of subscribers.

```http
GET /api/v1/subscribers
```

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `limit` | integer | Items per page (default: 50, max: 100) |
| `listId` | string | Filter by list ID |
| `status` | string | Filter by status: `pending`, `active`, `unsubscribed` |
| `tag` | string | Filter by tag |
| `search` | string | Search in email, firstName, lastName |

### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clxsub001",
      "email": "john@example.com",
      "firstName": "John",
      "lastName": "Doe",
      "status": "active",
      "tags": ["premium", "early-adopter"],
      "customFields": {
        "company": "Acme Inc.",
        "plan": "enterprise"
      },
      "source": "website",
      "listId": "clx456def",
      "createdAt": "2024-02-01T10:00:00.000Z",
      "updatedAt": "2024-03-10T15:30:00.000Z",
      "confirmedAt": "2024-02-01T10:05:00.000Z",
      "unsubscribedAt": null
    }
  ],
  "pagination": {
    "total": 5420,
    "page": 1,
    "limit": 50,
    "totalPages": 109,
    "hasMore": true
  }
}
```

### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/subscribers?listId=clx456def&status=active&tag=premium" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Get Subscriber

Retrieve a single subscriber by ID.

```http
GET /api/v1/subscribers/:id
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxsub001",
    "email": "john@example.com",
    "firstName": "John",
    "lastName": "Doe",
    "status": "active",
    "tags": ["premium", "early-adopter"],
    "customFields": {
      "company": "Acme Inc.",
      "plan": "enterprise"
    },
    "source": "website",
    "list": {
      "id": "clx456def",
      "name": "Newsletter"
    },
    "createdAt": "2024-02-01T10:00:00.000Z",
    "updatedAt": "2024-03-10T15:30:00.000Z",
    "confirmedAt": "2024-02-01T10:05:00.000Z",
    "unsubscribedAt": null
  }
}
```

---

## Create Subscriber

Add a new subscriber to a list.

```http
POST /api/v1/subscribers
```

### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `email` | string | ✓ | | Email address |
| `listId` | string | ✓ | | Target list ID |
| `firstName` | string | | | First name |
| `lastName` | string | | | Last name |
| `status` | string | | `active` | Status: `pending`, `active`, `unsubscribed` |
| `tags` | array | | | Array of tag strings |
| `customFields` | object | | | Key-value pairs for custom data |
| `source` | string | | `api` | Source identifier |
| `skipDoubleOptIn` | boolean | | false | Skip confirmation email |
| `consentText` | string | | | Effective consent wording to snapshot (max. 2,000 characters) |
| `privacyPolicyUrl` | string | | | Absolute HTTP(S) privacy-policy URL to include in the consent snapshot (max. 2,048 characters) |

### Request Example

```json
{
  "email": "jane@example.com",
  "listId": "clx456def",
  "firstName": "Jane",
  "lastName": "Smith",
  "tags": ["vip", "webinar-attendee"],
  "customFields": {
    "company": "Tech Corp",
    "role": "CTO"
  },
  "source": "webinar-signup"
}
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxsub002",
    "email": "jane@example.com",
    "firstName": "Jane",
    "lastName": "Smith",
    "status": "pending",
    "tags": ["vip", "webinar-attendee"],
    "customFields": {
      "company": "Tech Corp",
      "role": "CTO"
    },
    "source": "webinar-signup",
    "listId": "clx456def",
    "createdAt": "2024-03-16T14:00:00.000Z",
    "confirmedAt": null
  }
}
```

### Errors

- `404` - List not found
- `409` - Subscriber already exists in this list (returns `subscriberId`)

### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/subscribers" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "listId": "clx456def",
    "firstName": "Jane",
    "lastName": "Smith"
  }'
```

---

## Update Subscriber

Update an existing subscriber.

```http
PUT /api/v1/subscribers/:id
```

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | string | | New email address |
| `firstName` | string | | First name |
| `lastName` | string | | Last name |
| `status` | string | | Status: `pending`, `active`, `unsubscribed` |
| `tags` | array | | Replace all tags |
| `customFields` | object | | Replace all custom fields |

### Request Example

```json
{
  "tags": ["premium", "vip"],
  "customFields": {
    "company": "New Company Inc.",
    "plan": "premium"
  }
}
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxsub001",
    "email": "john@example.com",
    "firstName": "John",
    "lastName": "Doe",
    "status": "active",
    "tags": ["premium", "vip"],
    "customFields": {
      "company": "New Company Inc.",
      "plan": "premium"
    },
    "source": "website",
    "listId": "clx456def",
    "createdAt": "2024-02-01T10:00:00.000Z",
    "updatedAt": "2024-03-16T14:30:00.000Z",
    "confirmedAt": "2024-02-01T10:05:00.000Z",
    "unsubscribedAt": null
  }
}
```

### Errors

- `409` - Email already exists in this list

---

## Delete Subscriber

Permanently delete a subscriber.

```http
DELETE /api/v1/subscribers/:id
```

### Response

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "id": "clxsub001"
  }
}
```

---

# Templates

## List Templates

Retrieve a paginated list of email templates.

```http
GET /api/v1/templates
```

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `limit` | integer | Items per page (default: 50, max: 100) |
| `type` | string | Filter by type: `campaign`, `automation`, `transactional` |
| `category` | string | Filter by category |
| `search` | string | Search in template name |

### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clxtpl001",
      "name": "Newsletter Template",
      "subject": "{{newsletter_title}}",
      "type": "campaign",
      "category": "newsletter",
      "thumbnail": "https://cdn.mailaura.com/templates/thumb-001.png",
      "createdAt": "2024-01-15T09:00:00.000Z",
      "updatedAt": "2024-03-10T14:00:00.000Z"
    }
  ],
  "pagination": {
    "total": 12,
    "page": 1,
    "limit": 50,
    "totalPages": 1,
    "hasMore": false
  }
}
```

---

## Get Template

Retrieve a single template by ID including the HTML content.

```http
GET /api/v1/templates/:id
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxtpl001",
    "name": "Newsletter Template",
    "subject": "{{newsletter_title}}",
    "html": "<!DOCTYPE html><html>...</html>",
    "design": { ... },
    "type": "campaign",
    "category": "newsletter",
    "thumbnail": "https://cdn.mailaura.com/templates/thumb-001.png",
    "createdAt": "2024-01-15T09:00:00.000Z",
    "updatedAt": "2024-03-10T14:00:00.000Z"
  }
}
```

---

## Create Template

Create a new email template.

```http
POST /api/v1/templates
```

### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `name` | string | ✓ | | Template name (max 200 chars) |
| `subject` | string | | | Default subject line (max 500 chars) |
| `html` | string | ✓ | | HTML content |
| `design` | object | | | Legacy design JSON (deprecated) |
| `type` | string | | `campaign` | Type: `campaign`, `automation`, `transactional` |
| `category` | string | | | Category tag (max 50 chars) |

### Request Example

```json
{
  "name": "Product Launch Template",
  "subject": "Introducing {{product_name}}",
  "html": "<!DOCTYPE html><html><body><h1>{{product_name}}</h1><p>{{product_description}}</p></body></html>",
  "type": "campaign",
  "category": "product"
}
```

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxtpl002",
    "name": "Product Launch Template",
    "subject": "Introducing {{product_name}}",
    "type": "campaign",
    "category": "product",
    "createdAt": "2024-03-16T15:00:00.000Z",
    "updatedAt": "2024-03-16T15:00:00.000Z"
  }
}
```

---

## Update Template

Update an existing template.

```http
PUT /api/v1/templates/:id
```

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | | Template name (max 200 chars) |
| `subject` | string | | Subject line (max 500 chars, null to remove) |
| `html` | string | | HTML content |
| `design` | object | | Legacy design JSON (deprecated) |
| `type` | string | | Type: `campaign`, `automation`, `transactional` |
| `category` | string | | Category tag (max 50 chars, null to remove) |

### Response

```json
{
  "success": true,
  "data": {
    "id": "clxtpl002",
    "name": "Product Launch Template v2",
    "subject": "Introducing {{product_name}} - Now Available!",
    "type": "campaign",
    "category": "product",
    "createdAt": "2024-03-16T15:00:00.000Z",
    "updatedAt": "2024-03-16T16:00:00.000Z"
  }
}
```

---

## Delete Template

Delete a template. Templates that are used by campaigns cannot be deleted.

```http
DELETE /api/v1/templates/:id
```

### Response

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "id": "clxtpl002"
  }
}
```

### Errors

- `400` - Template is used by X campaign(s) and cannot be deleted
- `404` - Template not found

---

# Webhooks

The Webhooks endpoint allows external services (Zapier, Make, n8n, etc.) to trigger actions via HTTP requests.

## Webhook Actions

Execute various actions via webhook.

```http
POST /api/v1/webhooks
```

### Authentication

Same as other endpoints - use `X-API-Key` or `Authorization: Bearer` header.

---

### Add Subscriber

Add or update a subscriber in a list.

#### Request

```json
{
  "action": "add_subscriber",
  "listId": "clx456def",
  "email": "newuser@example.com",
  "firstName": "New",
  "lastName": "User",
  "customFields": {
    "source": "zapier",
    "plan": "free"
  },
  "tags": ["lead", "webinar"]
}
```

#### Response (Created)

```json
{
  "success": true,
  "action": "created",
  "subscriberId": "clxsub003"
}
```

#### Response (Updated - if subscriber exists)

```json
{
  "success": true,
  "action": "updated",
  "subscriberId": "clxsub003"
}
```

---

### Remove Subscriber

Remove a subscriber from a list.

#### Request

```json
{
  "action": "remove_subscriber",
  "listId": "clx456def",
  "email": "user@example.com"
}
```

#### Response

```json
{
  "success": true,
  "action": "removed",
  "email": "user@example.com"
}
```

---

### Tag Subscriber

Add tags to an existing subscriber.

#### Request

```json
{
  "action": "tag_subscriber",
  "listId": "clx456def",
  "email": "user@example.com",
  "tags": ["purchased", "premium"]
}
```

#### Response

```json
{
  "success": true,
  "action": "tagged",
  "subscriberId": "clxsub001",
  "tags": ["premium", "early-adopter", "purchased"]
}
```

---

## Webhook Documentation

Get information about available webhook actions.

```http
GET /api/v1/webhooks
```

### Response

```json
{
  "name": "Mailaura Webhook API",
  "version": "1.0",
  "documentation": "https://docs.mailaura.com/api/webhooks",
  "endpoints": {
    "POST /api/v1/webhooks": {
      "description": "Execute webhook actions",
      "authentication": "API Key via X-API-Key header or Bearer token",
      "actions": {
        "add_subscriber": { ... },
        "remove_subscriber": { ... },
        "tag_subscriber": { ... }
      }
    }
  }
}
```

---

## Zapier / Make Integration Examples

### Zapier

```
URL: https://www.mailaura.io/api/v1/webhooks
Method: POST
Headers:
  X-API-Key: ml_live_abc123xyz789
  Content-Type: application/json
Body:
  {
    "action": "add_subscriber",
    "listId": "YOUR_LIST_ID",
    "email": "{{email}}",
    "firstName": "{{first_name}}",
    "lastName": "{{last_name}}"
  }
```

### Make (Integromat)

Use the HTTP module with:
- **URL:** `https://www.mailaura.io/api/v1/webhooks`
- **Method:** POST
- **Headers:** `X-API-Key: YOUR_API_KEY`
- **Body type:** JSON
- **Body:** Map fields from your trigger

---

---

## Gallery Templates

Browse and preview the built-in email template gallery.

### `GET /gallery/templates`

List all gallery templates with optional category filter.

**Query Parameters:**
- `category` - Filter by category: `priority`, `ecommerce`, `engagement`, `content`, `feedback`, `business`
- `search` - Search by name or description

### `GET /gallery/templates/:id`

Get a single gallery template with full metadata and variable definitions.

### `GET /gallery/templates/:id/schema`

Get the structured template schema with sections, fields, and example values. This is the key endpoint for understanding what content a template needs.

### `GET /gallery/templates/:id/preview`

Get a rendered HTML preview with example content. Use `?format=html` for raw HTML output.

---

## Campaign Template Integration

Apply gallery templates to campaigns and manage template content.

### `POST /campaigns/:id/apply-template`

Apply a gallery template to a draft campaign.

**Body:**
```json
{
  "galleryTemplateId": "promotional"
}
```

### `PUT /campaigns/:id/template-values`

Update the template field values and re-render the campaign HTML. Supports partial updates.

**Body:**
```json
{
  "values": {
    "headline": "Summer Sale!",
    "bodyContent": "<p>Don't miss our biggest sale...</p>",
    "ctaText": "Shop Now"
  }
}
```

---

## AI Content

Generate AI-powered content for email templates.

### `POST /ai/generate-template-content`

Generate content for template fields using AI. Requires `write` scope. Uses AI credits.

**Body:**
```json
{
  "topic": "Black Friday Sale",
  "tone": "professional",
  "language": "de",
  "fields": [
    { "name": "headline", "type": "text", "label": "Headline", "maxLength": 60 },
    { "name": "bodyContent", "type": "richtext", "label": "Body Content" }
  ]
}
```

### `POST /ai/analyze-subject`

Analyze an email subject line for quality and get improvement suggestions.

**Body:**
```json
{
  "subject": "Don't miss our sale!!!",
  "language": "en"
}
```

**Response:**
```json
{
  "score": 65,
  "verdict": "okay",
  "suggestions": ["Reduce exclamation marks", "Add personalization"],
  "alternatives": ["Your exclusive deal awaits", "Save 30% today only"]
}
```

---

## RSS Campaigns

Create and manage RSS-based automated email campaigns.

### `GET /rss-campaigns`

List all RSS campaigns. Filter by `status` (draft, active, paused).

### `POST /rss-campaigns`

Create a new RSS campaign.

**Body:**
```json
{
  "name": "Blog Updates",
  "feedUrl": "https://blog.example.com/feed",
  "listId": "list_abc123",
  "checkInterval": "daily",
  "sendTime": "09:00",
  "subject": "New from {{feedTitle}}",
  "templateVariant": "default"
}
```

### `GET /rss-campaigns/:id`

Get full RSS campaign details including recent generated campaigns.

### `PUT /rss-campaigns/:id`

Update an RSS campaign's settings.

### `DELETE /rss-campaigns/:id`

Delete an RSS campaign.

### `POST /rss-campaigns/:id/activate`

Activate an RSS campaign to start monitoring the feed.

### `POST /rss-campaigns/:id/pause`

Pause an active RSS campaign.

### `GET /rss-campaigns/:id/stats`

Get statistics (sent, opened, clicked, open rate, click rate).

### `POST /rss-campaigns/:id/test`

Send a test email of the latest generated RSS digest.

**Required Scope:** `send`

**Body:**
```json
{
  "email": "test@example.com"
}
```

### `GET /rss-campaigns/:id/preview-feed`

Preview the latest items from the RSS feed.

**Required Scope:** `read`

**Response:**
```json
{
  "success": true,
  "data": {
    "feedUrl": "https://blog.example.com/feed",
    "itemCount": 5,
    "items": [
      {
        "title": "New Blog Post",
        "link": "https://blog.example.com/new-post",
        "pubDate": "2024-03-15T09:00:00.000Z",
        "description": "A short summary of the post..."
      }
    ]
  }
}
```

### `POST /rss-campaigns/trigger`

Trigger an immediate feed check for the organization's **active** RSS campaigns,
instead of waiting for the scheduled check. Used by the WordPress plugin's
publish hook.

**Required Scope:** `write`

**Body (optional):**
```json
{
  "feedUrl": "https://blog.example.com/feed"
}
```

Without `feedUrl` all active RSS campaigns of the organization are addressed.
Returns `404` if none match.

A campaign that already sent a newsletter today (in its own `sendTimezone`) is
**not** triggered — at most one newsletter goes out per send day, so a site that
publishes several posts a day does not mail its list once per post. Those
campaigns are reported under `skipped`:

```json
{
  "success": true,
  "data": {
    "message": "Triggered 1 RSS campaign(s) for processing",
    "triggered": [
      { "id": "rss_abc123", "name": "Blog Digest", "feedUrl": "https://blog.example.com/feed" }
    ],
    "skipped": [
      { "id": "rss_def456", "name": "News", "reason": "Already sent in this send window" }
    ]
  }
}
```

Triggering is asynchronous: a triggered campaign only sends if the feed actually
holds items that were not part of an earlier newsletter.

---

## Automations

Create and manage email automation workflows.

### `GET /automations`

List all automations. Filter by `status` (DRAFT, ACTIVE, PAUSED, ARCHIVED).

### `POST /automations`

Create a new automation.

**Body:**
```json
{
  "name": "Welcome Series",
  "triggerType": "SUBSCRIBER_ADDED",
  "listId": "list_abc123",
  "description": "Welcome new subscribers with a 3-email series"
}
```

**Available trigger types:** `SUBSCRIBER_ADDED`, `SUBSCRIBER_CONFIRMED`, `TAG_ADDED`, `TAG_REMOVED`, `DATE_FIELD`, `MANUAL`, `WEBHOOK`, `INACTIVITY`, `CART_ABANDONED`, `ORDER_SHIPPED`

### `GET /automations/:id`

Get automation with all steps and configuration.

### `PUT /automations/:id`

Update automation settings.

### `DELETE /automations/:id`

Delete an automation and all its steps.

### `POST /automations/:id/activate`

Activate an automation (requires at least one step).

### `POST /automations/:id/pause`

Pause an active automation.

### `GET /automations/:id/stats`

Get automation statistics (runs, sent, opened, clicked, rates).

### `GET /automations/templates`

List pre-built automation templates (Welcome Series, Re-Engagement, Birthday, etc.).

### `GET /automations/:id/steps`

List all steps in an automation in order.

**Required Scope:** `read`

### `POST /automations/:id/steps`

Add a new step to an automation.

**Required Scope:** `write`

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | ✓ | Step type: `EMAIL`, `DELAY`, `CONDITION`, `TAG_ADD`, `TAG_REMOVE`, `WEBHOOK`, etc. |
| `order` | integer | | Position in sequence (auto-appended if omitted) |
| `config` | object | | Step configuration JSON |
| `nodeId` | string | | React Flow node ID |
| `subject` | string | | Email subject (for `EMAIL` steps) |
| `templateId` | string | | Template ID (for `EMAIL` steps) |

**Example:**
```json
{
  "type": "EMAIL",
  "order": 1,
  "subject": "Welcome to our newsletter!",
  "templateId": "clxtpl001"
}
```

### `PUT /automations/:id/steps/:stepId`

Update an existing automation step.

**Required Scope:** `write`

### `DELETE /automations/:id/steps/:stepId`

Delete an automation step.

**Required Scope:** `write`

### `POST /automations/:id/trigger`

Manually trigger an automation for specific subscribers or (up to 1,000) active subscribers in the automation's list. Each subscriber is enqueued as an `automation/trigger` background event, i.e. the same path a signup trigger takes.

**Required Scope:** `send`

**Body (optional):**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `subscriberIds` | array | | Specific subscriber IDs, max 5,000 per call (triggers for the list if omitted). Only `active` subscribers are enrolled. |
| `startStepId` | string | | Resume at this step instead of the first one, e.g. to re-enrol subscribers whose earlier run was cut short. Must belong to the automation. |
| `deduplicationKey` | string | | A subscriber is enrolled at most once per automation and key (default `manual`). Use a fresh key such as `resume-2026-09` to deliberately re-enrol subscribers who already completed a run. 1–64 characters: letters, digits, `-`, `_`. |

**Example:**
```json
{
  "subscriberIds": ["clxsub001", "clxsub002"],
  "startStepId": "clxstep005",
  "deduplicationKey": "resume-2026-09"
}
```

**Response:**
```json
{
  "triggered": true,
  "subscribersQueued": 2,
  "subscribersSkipped": 0,
  "deduplicationKey": "resume-2026-09",
  "startStepId": "clxstep005"
}
```

`subscribersSkipped` counts requested IDs that were not active subscribers of this organization. A subscriber that already has a run for the same key, or is currently running through the automation, is skipped by the background job.

### `GET /automations/:id/runs`

Paginated list of automation run history.

**Required Scope:** `read`

**Query Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `limit` | integer | Items per page (default: 50, max: 100) |
| `status` | string | Filter by status: `RUNNING`, `WAITING`, `COMPLETED`, `FAILED`, `CANCELLED` |

### `POST /automations/:id/duplicate`

Create a copy of an automation with all its steps.

**Required Scope:** `write`

**Response (201):**
```json
{
  "success": true,
  "data": {
    "id": "clxaut999",
    "name": "Welcome Series (Copy)",
    "status": "DRAFT",
    "stepCount": 5,
    "createdAt": "2024-03-16T15:00:00.000Z"
  }
}
```

---

## Letters

Send physical letters via the Pingen postal API. Letters are printed, enveloped, and mailed to recipients automatically.

> **Note:** Letter sending requires a paid plan (Starter or above) and sufficient letter balance.
> Top up your balance in **Campaigns → Briefe → Guthaben aufladen**.

### Scopes

| Scope | Description |
|-------|-------------|
| `letters:read` | List letters, view templates, generate previews |
| `letters:write` | Send letters, cancel letters |

---

### POST /letters

Send a single physical letter via Pingen.

**Required Scope:** `letters:write`

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `templateId` | string | ✅ | | Letter template ID |
| `recipient` | object | ✅ | | Recipient address (see below) |
| `variables` | object | | `{}` | Template variable replacements |
| `options` | object | | | Print and delivery options (see below) |

**Recipient Object:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `company` | string | | | Company name |
| `salutation` | string | | | Salutation (e.g. "Herr", "Frau") |
| `firstName` | string | | | First name |
| `lastName` | string | | | Last name |
| `street` | string | ✅ | | Street address (min 1 char) |
| `zip` | string | ✅ | | Postal code (min 1 char) |
| `city` | string | ✅ | | City (min 1 char) |
| `country` | string | | `AT` | 2-character ISO country code |

**Options Object:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `printMode` | string | | `simplex` | `simplex` (single-sided) or `duplex` (double-sided) |
| `printSpectrum` | string | | `color` | `grayscale` or `color` |
| `deliveryProduct` | string | | `fast` | `cheap`, `fast`, or `registered` |
| `addressPosition` | string | | `left` | `left` or `right` |

#### Request Example

```json
{
  "templateId": "clx123abc",
  "recipient": {
    "company": "Acme GmbH",
    "salutation": "Herr",
    "firstName": "Max",
    "lastName": "Mustermann",
    "street": "Musterstraße 1",
    "zip": "1010",
    "city": "Wien",
    "country": "AT"
  },
  "variables": {
    "anrede": "Sehr geehrter Herr Mustermann",
    "vertragsnummer": "V-2026-001"
  },
  "options": {
    "printMode": "simplex",
    "printSpectrum": "color",
    "deliveryProduct": "fast",
    "addressPosition": "left"
  }
}
```

#### Response (201 Created)

```json
{
  "success": true,
  "data": {
    "id": "clxltr001",
    "status": "queued",
    "estimatedCost": {
      "value": 2.08,
      "currency": "EUR"
    },
    "createdAt": "2026-03-10T10:00:00.000Z"
  }
}
```

#### Errors

| Code | Description |
|------|-------------|
| `400` | Validation error (missing/invalid fields) |
| `402` | Insufficient letter balance |
| `403` | Plan does not support letter sending (Free plan) |
| `404` | Letter template not found |
| `429` | Rate limited (max 30 requests/minute) |

#### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/letters" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "clx123abc",
    "recipient": {
      "firstName": "Max",
      "lastName": "Mustermann",
      "street": "Musterstraße 1",
      "zip": "1010",
      "city": "Wien",
      "country": "AT"
    }
  }'
```

---

### GET /letters

List sent letters with pagination.

**Required Scope:** `letters:read`

#### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | integer | 1 | Page number |
| `limit` | integer | 50 | Items per page (max: 100) |
| `status` | string | | Filter by status: `queued`, `submitted`, `processing`, `sent`, `delivered`, `cancelled`, `failed` |
| `campaign_id` | string | | Filter by letter campaign ID |

#### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clxltr001",
      "templateId": "clx123abc",
      "templateName": "Willkommensbrief",
      "status": "sent",
      "recipient": {
        "firstName": "Max",
        "lastName": "Mustermann",
        "city": "Wien",
        "country": "AT"
      },
      "cost": {
        "value": 2.08,
        "currency": "EUR"
      },
      "pingenStatus": "sent",
      "trackingNumber": "RM123456789AT",
      "campaignId": null,
      "createdAt": "2026-03-10T10:00:00.000Z",
      "updatedAt": "2026-03-10T12:30:00.000Z"
    }
  ],
  "pagination": {
    "total": 42,
    "page": 1,
    "limit": 50,
    "totalPages": 1,
    "hasMore": false
  }
}
```

#### Example

```bash
curl -X GET "https://www.mailaura.io/api/v1/letters?status=sent&limit=10" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

### POST /letters/preview

Generate a PDF preview of a letter template without sending it.

**Required Scope:** `letters:read`

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `templateId` | string | ✅ | Letter template ID |
| `recipient` | object | | Recipient data for address window preview |
| `variables` | object | | Template variables to substitute |

#### Request Example

```json
{
  "templateId": "clx123abc",
  "recipient": {
    "firstName": "Max",
    "lastName": "Mustermann",
    "street": "Musterstraße 1",
    "zip": "1010",
    "city": "Wien"
  },
  "variables": {
    "anrede": "Sehr geehrter Herr Mustermann"
  }
}
```

#### Response

```json
{
  "success": true,
  "data": {
    "base64": "JVBERi0xLjQKMSAwIG9iago8PA...",
    "mimeType": "application/pdf"
  }
}
```

#### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/letters/preview" \
  -H "X-API-Key: ml_live_abc123xyz789" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "clx123abc"
  }'
```

---

### GET /letters/templates

List available letter templates.

**Required Scope:** `letters:read`

#### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | integer | 1 | Page number |
| `limit` | integer | 50 | Items per page (max: 100) |

#### Response

```json
{
  "success": true,
  "data": [
    {
      "id": "clx123abc",
      "name": "Willkommensbrief",
      "senderName": "Acme GmbH",
      "addressPosition": "left",
      "createdAt": "2026-01-15T09:00:00.000Z",
      "updatedAt": "2026-03-10T14:00:00.000Z"
    }
  ],
  "pagination": {
    "total": 5,
    "page": 1,
    "limit": 50,
    "totalPages": 1,
    "hasMore": false
  }
}
```

---

### POST /letters/:id/cancel

Cancel a queued letter immediately, or request cancellation of a provider-confirmed processing letter. Uncertain submissions and letters already printing or sent cannot be cancelled locally. Refunds require confirmed cancellation.

**Required Scope:** `letters:write`

#### Response

```json
{
  "success": true,
  "data": {
    "id": "clxltr001",
    "status": "cancelled",
    "refunded": true,
    "refundAmount": {
      "value": 2.08,
      "currency": "EUR"
    }
  }
}
```

#### Errors

| Code | Description |
|------|-------------|
| `400` | Letter cannot be cancelled (already printing/sent/delivered) |
| `404` | Letter not found |

#### Example

```bash
curl -X POST "https://www.mailaura.io/api/v1/letters/clxltr001/cancel" \
  -H "X-API-Key: ml_live_abc123xyz789"
```

---

## Support

For API support or questions:

- **Documentation:** https://docs.mailaura.com
- **Email:** office@mailaura.io
- **Status Page:** https://status.mailaura.com












### Editable campaign content

`POST /api/v1/campaigns` and `PUT /api/v1/campaigns/:id` accept optional `editorTemplateId` (the editor slug, e.g. `minimal-announcement`) and `templateValues` (a string-to-string object with fields such as `headline`, `subheadline`, `bodyContent`, `ctaText`, and `ctaUrl`). Supply these together with rendered `html` when a campaign must be editable in the dashboard. The saved-template database `templateId` is a different identifier and does not populate the editor. `GET /api/v1/campaigns/:id` returns the editor state. Omitted editor fields on PUT remain unchanged.


## Letter campaign extensions (audit follow-up #12)

See [Physical letter campaigns](./letter-campaigns.md#rest-and-mcp) for the new draft/list/status/estimate/send/resume API and private PDF download.

Single-letter POST accepts `Idempotency-Key` (or `idempotencyKey` in JSON). Reuse it for retries of the same content; different content returns 409. New statuses are `cancel_requested` (waiting for provider confirmation) and `review_required` (submission outcome uncertain; never automatically resubmit). Cancellation of queued mail refunds immediately; provider mail refunds only after confirmed cancellation. Returned mail is not refunded. Letter estimates account for actual rendered pages and recipient destinations; final provider postage is reconciled separately.

### Campaign plain-text alternatives (#215)

`POST /api/v1/campaigns` and `PUT /api/v1/campaigns/:id` accept optional `text`.
A non-blank string stores an authored alternative (maximum 100,000 UTF-8 bytes);
`null` restores automatic generation. Omitting the property on update preserves
the current value. `GET /api/v1/campaigns/:id` returns the stored `text` or `null`.
Duplication copies it. HTML edits do not silently overwrite authored text.

Test and production sends apply the same merge-tag and greeting-fallback rules
to the saved alternative, using test data for test recipients. Missing recipient
unsubscribe and online-version links are appended, followed by any required
Mailaura badge. Automatic text is generated structurally from finalized HTML,
including link destinations, paragraphs, list items and image alternatives.
Review authored text after changing HTML; use `text: null` to follow HTML again.
REST test delivery now uses the common test sender and returns an error if the
transport rejects delivery.

### Campaign link validation

`POST /api/v1/campaigns/{id}/validate-links` requires `send` scope and the campaign must belong to the API key's organisation. An empty body runs a bounded advisory check of saved HTML/text before tracking. The response includes `validationId`, `fingerprint`, `checkedAt`, redacted link inventory, redirect chains, HTTP status, findings (`blocker`, `warning`, `info`), counts and `truncated`. Action/personalised/tracking destinations are never probed. HTTP GET requests to ordinary public destinations use DNS-pinned SSRF protection and discard response bodies.

A JSON body `{ "validationId": "…", "reason": "Verified manually in the browser" }` records an explicit review of warnings and additionally requires `write` scope. Reasons are 10–500 characters. Blocking findings cannot be overridden. Changed content and checks older than 24 hours require a fresh run. Concurrent checks share a tenant-wide 30-second cooldown (429). No links are crawled automatically on send or idempotent replay. Send/schedule audit metadata records `checked`, `stale`, `not_checked` or `unavailable` explicitly; this MVP is advisory, not a mandatory delivery gate. See [campaign link preflight](campaign-link-preflight.md).


## Account and transactional tracking

- `GET /account` (`read`): returns `plan`, `planName`, `features.emailAttachments`, `features.automation`, and contact/email limits.
- `GET /account/tracking` (`read`): returns `data.transactional_tracking`.
- `PUT /account/tracking` (`write`): body `{ "transactional_tracking": true }`; returns the stored setting. Tracking is off by default and applies across transactional and WordPress sending. The `/send` `no_tracking` flag can suppress it per message.

## Sender domains

`GET /domains`, `GET /domains/:id`, and `GET /domains/:id/dns` require `domains:read` or `read`. The DNS endpoint accepts an optional `purpose` filter and returns the current setup records. `POST /domains`, `POST /domains/:id/verify`, `POST /domains/:id/mail-from`, and `DELETE /domains/:id` require `domains:write` or `write`.

Create with `{ "domain": "example.com", "dmarcReportEmail": "dmarc@example.com" }` (report email optional). Then retrieve the DNS records, apply them with the DNS provider, and check verification. A DNS match alone does not establish final SES verification. Never create a second SPF record; merge the required include into an existing record. MAIL FROM requires an already verified domain and accepts `{ "subdomain": "bounce" }` (default `bounce`). Apply its additional MX/SPF records before relying on it.

## Presigned attachments and WordPress HTTP delivery

`POST /attachment/presign` requires standard API `write` plus API and attachment entitlements. Body: `{ "filename": "invoice.pdf", "contentType": "application/pdf", "size": 12345 }`. Maximum declared size is 25 MiB. The response contains `uploadUrl`, `s3Key`, `expiresIn` (900 seconds) and `maxSize`. Upload with HTTP PUT to that URL and the matching content type, then use the returned `s3Key` in a supported email attachment. Temporary objects are subject to storage cleanup.

`POST /wordpress` requires a standard `write` API key. It sends system mail through the transactional service, not SMTP. Required fields are `to`, `subject`, and at least one of `html`/`text`. Optional inputs include `from`, `from_name`, `cc`, `bcc`, `reply_to`, `tags`, `metadata`, `priority`, `skip_footer`, `wrap_in_template`, and attachments (`content` as base64 or `s3Key`). Supports `Idempotency-Key`. Check the response for attachments omitted due to plan restrictions.

## WooCommerce integration operations

- `POST /woocommerce/sync` requires `write` or `subscribers:write`. Send a `customers` array using WooCommerce customer fields, optionally `listId`. An explicit list must belong to the authenticated organization; otherwise the configured default or first owned list is used. Inspect per-customer results.
- `POST /woocommerce/data` requires `write`. Its `event` discriminator selects `cart_abandoned`, `order_placed`, `order_shipped`, `order_completed`, `product_published` or `product_on_sale`. The OpenAPI request schema lists the distinct required fields for each event. Event ingestion does not bypass consent or configured automation/sending gates.
- `POST /woocommerce/webhook` is a callback using the configured secret and `X-WC-Webhook-Signature`: base64 HMAC-SHA256 over the raw body. Supply organization via `org` query or `X-Organization-ID`. This is not standard API-key authentication. `GET` provides setup information; it does not synchronize customers.

## Physical letter campaign operations

Letter reads require `letters:read` or `read`; campaign mutations require `letters:write` or `write`, including estimates.

- `GET /letters/campaigns`: `page` pagination, fixed 25 records per page.
- `POST /letters/campaigns`: `{ "name": "October update", "listId": "LIST_ID", "templateId": "LETTER_TEMPLATE_ID", "options": {} }` creates a draft without dispatch.
- `GET /letters/campaigns/:id`: retrieves the owned postal campaign.
- `POST /letters/campaigns/:id`: `{ "action": "estimate" }`, `send` or `resume`. Estimate uses a provider quote; send/resume can reserve credit and dispatch. Provider readiness, valid postal addresses, plan eligibility and sufficient balance/allowance are required.
- `GET /letters/:id`: retrieves an individual delivery log. `GET /letters/:id/pdf` returns its archived private `application/pdf`, accepting a permitted API key or an authenticated organization session. Missing archive returns 404.
- `PUT /letters/templates/:id`: updates name/description/content. Supplied nested `content` replaces its prior content rather than merging individual fields. `DELETE` removes an owned letter template.

Submitted, printed, sent and delivered are distinct statuses. A cancellation request does not itself prove cancellation or refund. Do not submit a second physical letter merely because the first request had an uncertain response; use its idempotency key and inspect the existing log.

## API reference scope and maintenance

The public OpenAPI specification and operation directory cover customer routes and integration callbacks. REST, MCP and CLI coverage are different; the existence of a REST operation does not imply a corresponding MCP tool or CLI command. The customer dashboard is session-authenticated and also uses internal APIs not exposed by REST v1.


## Complete operation directory

- **GET /subscribers**: Abonnenten auflisten. Gibt eine paginierte Liste aller Abonnenten zurück
- **POST /subscribers**: Abonnenten erstellen. Erstellt einen neuen Abonnenten in einer Liste Requires write scope. consentText and privacyPolicyUrl are saved as a consent snapshot. Do not use skipDoubleOptIn without a valid pre-existing consent basis.
- **GET /subscribers/{id}**: Abonnent abrufen. Gibt einen einzelnen Abonnenten zurück
- **PUT /subscribers/{id}**: Abonnent aktualisieren. Aktualisiert einen bestehenden Abonnenten
- **DELETE /subscribers/{id}**: Abonnent löschen. Löscht einen Abonnenten permanent
- **GET /lists**: Listen auflisten. Gibt eine paginierte Liste aller Abonnenten-Listen zurück
- **POST /lists**: Liste erstellen. Erstellt eine neue Abonnenten-Liste
- **GET /lists/{id}**: Liste abrufen. Gibt eine einzelne Liste mit Statistiken zurück
- **PUT /lists/{id}**: Liste aktualisieren.
- **DELETE /lists/{id}**: Liste löschen. Deletes the list and its subscribers. Campaign content and history are retained; recipient assignments are removed and sends are paused.
- **GET /campaigns**: Kampagnen auflisten.
- **POST /campaigns**: Kampagne erstellen. Draft listId may be omitted or null; an owned recipient list is required before scheduling/sending. html is required by the REST create contract even when empty. editorTemplateId and templateValues retain editable template state; rendered html alone does not. text omitted/null follows generated HTML; custom text must be nonblank and no more than 100 KB UTF-8.
- **GET /campaigns/{id}**: Kampagne abrufen.
- **PUT /campaigns/{id}**: Kampagne aktualisieren. Nur Entwürfe und geplante Kampagnen können aktualisiert werden Only allowed campaign states may be edited. listId may be cleared for a draft, but sending requires an owned recipient list. text:null resets automatic plain text; custom text must be nonblank and no more than 100 KB UTF-8.
- **DELETE /campaigns/{id}**: Kampagne löschen. Nur Entwürfe und geplante Kampagnen können gelöscht werden
- **POST /campaigns/{id}/validate-links**: Validate saved campaign links. Requires send scope. Empty body runs an advisory, bounded link check. An override body records manual review and additionally requires write scope. Blocking findings cannot be overridden. No delivery is triggered. A tenant-wide 30-second cooldown applies. Results expire after 24 hours or content changes.
- **POST /campaigns/{id}/send**: Kampagne senden. Requires send scope. Validates the saved audience, DOI eligibility, sender trust and billing, then queues asynchronous delivery. HTTP 202 confirms queue acceptance, not recipient delivery. Reuse Idempotency-Key for the same retry; the default logical key is derived from the organization-scoped campaign ID. Inspect the existing jobId before any retry.
- **GET /campaigns/{id}/stats**: Kampagnen-Statistiken abrufen. Gibt detaillierte Statistiken für eine Kampagne zurück
- **GET /templates**: Vorlagen auflisten.
- **POST /templates**: Vorlage erstellen.
- **GET /templates/{id}**: Vorlage abrufen.
- **PUT /templates/{id}**: Vorlage aktualisieren.
- **DELETE /templates/{id}**: Vorlage löschen.
- **POST /send**: Transaktionale E-Mail senden. Sendet eine einzelne transaktionale E-Mail (Passwort-Reset, Bestellbestätigung, etc.). **Authentifizierung:** Verwendet separate API-Keys mit dem Präfix `ml_trans_live_` oder `ml_trans_test_`. **Sandbox Mode:** API-Keys mit `ml_trans_test_` Präfix senden keine echten E-Mails, sondern speichern sie nur in der Datenbank für Tests. **Priority:** - `critical`: Umgeht Rate Limits (z.B. Passwort-Reset) - `high`: Bevorzugte Verarbeitung - `normal`: Standard-Verarbeitung - `low`: Verzögerte Verarbeitung At least one of html, text or template_slug is required. An attachment must have content or s3Key. Tracking defaults to the organization setting; no_tracking can suppress it per message. SMTP relay is disabled. At least one of html, text or template_slug is required. An attachment must have content or s3Key. Tracking defaults to the organization setting; no_tracking can suppress it per message. SMTP relay is disabled.
- **GET /webhooks**: Read supported incoming webhook actions. Standard API read scope. Returns incoming action documentation. These are contact-management webhook actions, separate from outgoing transactional lifecycle subscriptions.
- **POST /webhooks**: Webhook-Aktion ausführen. Führt eine Webhook-Aktion aus. Unterstützte Aktionen: - `add_subscriber`: Abonnent hinzufügen/aktualisieren - `remove_subscriber`: Abonnent entfernen - `tag_subscriber`: Tags zu Abonnent hinzufügen
- **GET /gallery/templates**: List gallery templates. List all available email gallery templates with optional category filter
- **GET /gallery/templates/{id}**: Get gallery template details.
- **GET /gallery/templates/{id}/schema**: Get template schema. Get structured schema with sections, fields, and example values
- **GET /gallery/templates/{id}/preview**: Preview template with example data.
- **POST /campaigns/{id}/apply-template**: Apply gallery template to campaign.
- **PUT /campaigns/{id}/template-values**: Update campaign template values.
- **POST /ai/generate-template-content**: Generate AI content for template fields.
- **POST /ai/analyze-subject**: Analyze email subject line.
- **GET /rss-campaigns**: List RSS campaigns.
- **POST /rss-campaigns**: Create RSS campaign. Draft listId may be omitted or null; activation requires a recipient list. checkInterval controls polling; sendTime, sendTimezone and sendDays control send windows. At most one newsletter per send day, including immediate trigger checks. accentColor is optional and can be null to inherit headerColor.
- **GET /rss-campaigns/{id}**: Get RSS campaign.
- **PUT /rss-campaigns/{id}**: Update RSS campaign.
- **DELETE /rss-campaigns/{id}**: Delete RSS campaign.
- **POST /rss-campaigns/{id}/activate**: Activate RSS campaign.
- **POST /rss-campaigns/{id}/pause**: Pause RSS campaign.
- **GET /rss-campaigns/{id}/stats**: Get RSS campaign statistics.
- **GET /automations**: List automations.
- **POST /automations**: Create automation. The REST trigger enum differs from the dashboard; do not assume every dashboard trigger is available here. Creating an automation does not activate it.
- **GET /automations/{id}**: Get automation with steps.
- **PUT /automations/{id}**: Update automation.
- **DELETE /automations/{id}**: Delete automation.
- **POST /automations/{id}/activate**: Activate automation.
- **POST /automations/{id}/pause**: Pause automation.
- **GET /automations/{id}/stats**: Get automation statistics.
- **GET /automations/templates**: List automation templates. Browse pre-built automation workflows
- **GET /automations/{id}/steps**: Automations-Schritte auflisten. Gibt alle Schritte einer Automation zurück
- **POST /automations/{id}/steps**: Automations-Schritt hinzufügen. Fügt einen neuen Schritt zur Automation hinzu
- **PUT /automations/{id}/steps/{stepId}**: Automations-Schritt aktualisieren. Aktualisiert einen bestehenden Automations-Schritt
- **DELETE /automations/{id}/steps/{stepId}**: Automations-Schritt löschen. Löscht einen Schritt aus der Automation
- **POST /automations/{id}/trigger**: Automation manuell auslösen. Löst eine Automation manuell aus. Erfordert den Scope `send`.
- **GET /automations/{id}/runs**: Automations-Ausführungen auflisten. Gibt eine paginierte Liste der Ausführungen einer Automation zurück
- **POST /automations/{id}/duplicate**: Automation duplizieren. Dupliziert eine Automation inklusive aller Schritte
- **GET /tags**: Tags auflisten. Gibt eine Liste aller Tags mit Anzahl der zugeordneten Abonnenten zurück
- **POST /tags**: Tag erstellen und zuweisen. Erstellt einen Tag und weist ihn optional Abonnenten zu
- **PUT /tags/{name}**: Tag umbenennen. Benennt einen bestehenden Tag um
- **DELETE /tags/{name}**: Tag löschen. Löscht einen Tag von allen Abonnenten
- **POST /tags/{name}/assign**: Tag Abonnenten zuweisen. Weist einen Tag einer Liste von Abonnenten zu
- **POST /tags/{name}/remove**: Tag von Abonnenten entfernen. Entfernt einen Tag von einer Liste von Abonnenten
- **GET /media**: Medien auflisten. Gibt eine paginierte Liste aller Mediendateien zurück
- **GET /media/{id}**: Medium abrufen. Gibt ein einzelnes Medium mit Nutzungsinformationen zurück
- **PUT /media/{id}**: Medium-Metadaten aktualisieren. Aktualisiert Alt-Text und Titel einer Mediendatei
- **DELETE /media/{id}**: Medium löschen. Löscht eine Mediendatei aus S3 und der Datenbank
- **GET /media/quota**: Speicherkontingent abrufen. Gibt das aktuelle Speicherkontingent und die Nutzung zurück
- **GET /brand**: Read organization branding. Standard API read scope. Returns current organization branding; PUT updates only the fields accepted by its schema.
- **PUT /brand**: Branding aktualisieren. Aktualisiert die Branding-Einstellungen der Organisation
- **POST /brand/analyze**: Website-Branding analysieren. Analysiert eine Website und extrahiert Markenfarben. Nur für Pro-Pläne verfügbar.
- **POST /campaigns/{id}/test**: Test-E-Mail senden. Sendet eine Test-E-Mail der Kampagne an eine angegebene Adresse. Erfordert den Scope `send`.
- **POST /campaigns/{id}/duplicate**: Kampagne duplizieren. Erstellt eine Kopie einer bestehenden Kampagne. Erfordert den Scope `write`.
- **POST /campaigns/{id}/pause**: Kampagne pausieren. Pausiert eine Kampagne, die gerade gesendet wird
- **POST /campaigns/{id}/resume**: Kampagne fortsetzen. Setzt eine pausierte Kampagne fort
- **GET /analytics/overview**: Account-Übersicht abrufen. Gibt eine Übersicht der wichtigsten Kennzahlen des gesamten Accounts zurück
- **GET /analytics/engagement**: Engagement-Trends abrufen. Gibt Engagement-Trends über einen Zeitraum zurück
- **GET /analytics/devices**: Geräte-Statistiken abrufen. Gibt Geräte- und Browser-Statistiken zurück
- **GET /profile**: Organisationsprofil abrufen. Gibt das Profil der aktuellen Organisation zurück
- **PUT /profile**: Organisationsprofil aktualisieren. Aktualisiert das Profil der Organisation
- **GET /api-keys**: API-Schlüssel auflisten. Lists this organization's key metadata, including nullable expiry; never returns full keys or hashes. Requires read scope.
- **POST /api-keys**: API-Schlüssel erstellen. Creates a key in the caller's organization. Requires write scope. Requested scopes must be covered by the caller (exact scope or wildcard). Omitted expiry inherits the caller's finite expiry, otherwise no expiry. Explicit null means no expiry and is forbidden for an expiring caller. An explicit expiry must be a future ISO 8601 timestamp with Z or a numeric timezone offset and cannot exceed the caller's expiry, even for wildcard callers. Returns the full key only once. For replacement, create a new key with no broader scopes, store it securely, update and verify the integration, then delete the old key. Creation still requires a spare plan quota slot.
- **DELETE /api-keys/{id}**: API-Schlüssel löschen. Löscht einen API-Schlüssel permanent
- **POST /rss-campaigns/{id}/test**: RSS-Kampagne testen. Sendet eine Test-E-Mail der RSS-Kampagne an eine angegebene Adresse
- **GET /rss-campaigns/{id}/preview-feed**: Feed-Vorschau abrufen. Gibt eine Vorschau der aktuellen Feed-Einträge zurück
- **GET /domains/{id}**: Get sender domain. Requires domains:read or read for reads; domains:write or write for mutations. Registration returns pending status; DNS and SES checks are still required. Suspended organizations are read-only and held domains may return 403.
- **DELETE /domains/{id}**: Remove sender domain. Requires domains:read or read for reads; domains:write or write for mutations. Registration returns pending status; DNS and SES checks are still required. Suspended organizations are read-only and held domains may return 403.
- **PUT /letters/templates/{id}**: Update a letter template. Requires letters:write or write. Content is a complete replacement when supplied, not a partial merge of nested fields.
- **DELETE /letters/templates/{id}**: Delete a letter template. Requires letters:write or write. Content is a complete replacement when supplied, not a partial merge of nested fields.
- **GET /transactional/templates/{slug}**: Read template languages by slug. Accepts transactional credentials or a standard API key. GET requires read or templates; PUT/DELETE require write or templates. GET without language returns all languages for the slug; mutation language defaults to de.
- **PUT /transactional/templates/{slug}**: Update a transactional template language. Accepts transactional credentials or a standard API key. GET requires read or templates; PUT/DELETE require write or templates. GET without language returns all languages for the slug; mutation language defaults to de.
- **DELETE /transactional/templates/{slug}**: Delete a transactional template language. Accepts transactional credentials or a standard API key. GET requires read or templates; PUT/DELETE require write or templates. GET without language returns all languages for the slug; mutation language defaults to de.
- **PATCH /transactional/webhooks/{id}**: Update a transactional webhook subscription. Requires write or webhooks:write using transactional credentials or a standard API key. PATCH requires at least one changed field. Responses omit the secret.
- **DELETE /transactional/webhooks/{id}**: Delete a transactional webhook subscription. Requires write or webhooks:write using transactional credentials or a standard API key. PATCH requires at least one changed field. Responses omit the secret.
- **GET /account**: Read organization plan and limits. Standard API key with read scope. Returns plan, planName, features.emailAttachments, features.automation and limits.emailsPerMonth/contacts.
- **GET /account/tracking**: Read transactional tracking preference. Standard API key: read for GET, write for PUT. Returns data.transactional_tracking. Default organization tracking is off; no_tracking can suppress tracking on an individual transactional send.
- **PUT /account/tracking**: Update transactional tracking preference. Standard API key: read for GET, write for PUT. Returns data.transactional_tracking. Default organization tracking is off; no_tracking can suppress tracking on an individual transactional send.
- **GET /domains**: List sender domains. Requires domains:read or read for reads; domains:write or write for mutations. Registration returns pending status; DNS and SES checks are still required. Suspended organizations are read-only and held domains may return 403.
- **POST /domains**: Register a sender domain. Requires domains:read or read for reads; domains:write or write for mutations. Registration returns pending status; DNS and SES checks are still required. Suspended organizations are read-only and held domains may return 403.
- **GET /domains/{id}/dns**: Get required DNS records. Requires domains:read or read. Returns setup records, not a successful delivery guarantee. Never publish a second SPF record; merge an existing SPF configuration.
- **GET /emails**: List transactional emails with cursor pagination. Requires read scope. Accepts transactional credentials or a standard API key. Returns data.emails and data.next_cursor. Supply the cursor verbatim for the next page; this endpoint does not use page pagination.
- **GET /emails/{id}**: Read transactional email delivery status. Requires read scope. Accepts transactional credentials or a standard API key. Returns a sanitized delivery log; sent does not mean delivered. See the text reference for response fields.
- **GET /letters**: List individual letter logs. Reads require letters:read or read. Sending requires letters:write or write, eligible letter plan and provider readiness, and an additional 30 requests/minute send limit. POST queues one letter and can debit postage. Reuse the same Idempotency-Key for a retry of the same request.
- **POST /letters**: Queue one physical letter. Reads require letters:read or read. Sending requires letters:write or write, eligible letter plan and provider readiness, and an additional 30 requests/minute send limit. POST queues one letter and can debit postage. Reuse the same Idempotency-Key for a retry of the same request.
- **GET /letters/campaigns**: List physical letter campaigns. Requires letters:read/read for GET and letters:write/write for POST. POST creates a draft using an owned list and letter template; it does not dispatch. GET uses page with a fixed page size of 25.
- **POST /letters/campaigns**: Create a physical letter campaign draft. Requires letters:read/read for GET and letters:write/write for POST. POST creates a draft using an owned list and letter template; it does not dispatch. GET uses page with a fixed page size of 25.
- **GET /letters/campaigns/{id}**: Read a physical letter campaign. Requires letters:read/read for GET and letters:write/write for POST, including estimate. action=estimate returns a current provider quote; send/resume can reserve credit and dispatch. Insufficient credit, unavailable provider or invalid addresses prevent dispatch.
- **POST /letters/campaigns/{id}**: Estimate, send or resume a physical letter campaign. Requires letters:read/read for GET and letters:write/write for POST, including estimate. action=estimate returns a current provider quote; send/resume can reserve credit and dispatch. Insufficient credit, unavailable provider or invalid addresses prevent dispatch.
- **GET /letters/templates**: List letter templates. Requires letters:read/read for GET and letters:write/write for POST. Only organization-owned letter templates are returned.
- **POST /letters/templates**: Create a letter template. Requires letters:read/read for GET and letters:write/write for POST. Only organization-owned letter templates are returned.
- **GET /letters/{id}**: Read a letter log. Reads require letters:read or read. Sending requires letters:write or write, eligible letter plan and provider readiness, and an additional 30 requests/minute send limit. POST queues one letter and can debit postage. Reuse the same Idempotency-Key for a retry of the same request.
- **GET /letters/{id}/pdf**: Read the private archived letter PDF. Requires letters:read/read API scope or an authenticated session with access to the active organization. Returns raw application/pdf with private, no-store caching. Returns 404 if the archived PDF is unavailable.
- **GET /transactional/templates**: List transactional templates. Accepts transactional credentials or a standard API key. GET requires read or templates; POST requires write or templates. language defaults to de for creation.
- **POST /transactional/templates**: Create a transactional template. Accepts transactional credentials or a standard API key. GET requires read or templates; POST requires write or templates. language defaults to de for creation.
- **GET /transactional/webhooks**: List transactional webhook subscriptions. Accepts transactional credentials or a standard API key. GET requires read; POST requires write or webhooks:write and the organization webhook quota. Creation returns data.webhook plus data.secret; the secret is shown only once. Events use EMAIL_* names.
- **POST /transactional/webhooks**: Create a transactional webhook subscription. Accepts transactional credentials or a standard API key. GET requires read; POST requires write or webhooks:write and the organization webhook quota. Creation returns data.webhook plus data.secret; the secret is shown only once. Events use EMAIL_* names.
- **GET /woocommerce/webhook**: Read WooCommerce webhook configuration information. Integration callback, not a general newsletter-send endpoint. POST uses the configured WooCommerce secret and X-WC-Webhook-Signature (HMAC-SHA256 of the raw body, base64), plus org query or X-Organization-ID. The organization integration must be configured. GET is an unauthenticated setup-information response.
- **POST /woocommerce/webhook**: Receive a signed WooCommerce webhook. Integration callback, not a general newsletter-send endpoint. POST uses the configured WooCommerce secret and X-WC-Webhook-Signature (HMAC-SHA256 of the raw body, base64), plus org query or X-Organization-ID. The organization integration must be configured. GET is an unauthenticated setup-information response.
- **POST /attachment/presign**: Create a temporary attachment upload URL. Requires standard API write scope, API access and emailAttachments entitlement. Return data.uploadUrl, s3Key, expiresIn (900 seconds), maxSize. PUT the file to uploadUrl using the matching Content-Type, then pass s3Key to the supported email attachment input. Temporary attachments expire by storage lifecycle.
- **POST /domains/{id}/mail-from**: Enable custom MAIL FROM. Requires domains:write or write and an already verified domain. Empty body defaults the subdomain to bounce. Add the returned MX and SPF records and check status again.
- **POST /domains/{id}/verify**: Check DNS and SES verification. Requires domains:write or write. Checks DNS and SES state. A DNS match alone does not establish final SES verification.
- **POST /letters/preview**: Render a letter template preview as base64 PDF. Requires letters:read or read; additional 60 requests/minute limit. Returns data.base64 and data.mimeType=application/pdf. This endpoint renders a saved template; it does not send or debit postage.
- **POST /letters/{id}/cancel**: Request cancellation of a physical letter. Requires letters:write or write. Cancellation depends on provider state; requesting cancellation does not establish a confirmed cancellation or refund.
- **POST /rss-campaigns/trigger**: Trigger active RSS campaign checks. Requires standard API write scope. Triggers active RSS campaigns in the organization, optionally by exact feedUrl. At most one newsletter per send day in the campaign timezone; already-sent campaigns appear in skipped. A queued check is not a delivery confirmation.
- **POST /woocommerce/data**: Process a WooCommerce cart, order or product event. Requires standard API write scope. Supply one supported discriminated event payload. The event drives the corresponding configured automations; configuration, consent and sending gates still apply.
- **POST /woocommerce/sync**: Sync WooCommerce customers to an owned list. Requires write or subscribers:write on a standard API key. listId must belong to the authenticated organization. If absent, the configured default list or first owned list is used. Processes the customers array and reports individual results.
- **POST /wordpress**: Send a WordPress system email through HTTP. Requires standard API write scope. Sends WordPress system email through the transactional pipeline; HTTP API only, SMTP relay is disabled. Requires html or text, supports base64 attachments or presigned s3Key, optional wrapping and Idempotency-Key. Attachment entitlement still applies; inspect the response for omitted attachments.
