---
title: "Email AI-CRM API"
description: "Admin email CRM routes (channels + sourceChannel), cron processor, cleanup tokens note, and inbound webhook"
locale: "en"
---
# Email AI-CRM API

All `/api/admin/email/*` routes require an authenticated **platform admin** session (`admin` or `superadmin` role).

Review UI lives at **`/admin/crm/*`** (`CrmAdminShell`). API paths stay `/api/admin/email/*` — see [Email AI-CRM](/docs/features/email-ai-crm.md).

`/api/cron/*` and `/api/webhooks/email/inbound` use `CRON_SECRET` or `WEBHOOK_EMAIL_SECRET` — not user sessions. Fail-closed when secret missing.

Draft **send** uses `EmailSenderService` + channel SMTP (`CRM_CHANNEL_*`). Auth OTP uses `lib/mailer.ts` / `SMTP_*`. Do not conflate the two in ops runbooks.

## Admin — channels

### `GET /api/admin/email/channels`

Read-only CRM channel status (no passwords). Backs the inbox channel filter UI.

**Response:**

```json
{
  "channels": [
    {
      "id": "primary",
      "name": "Primary",
      "flow": "standard",
      "mailbox": "INBOX",
      "imapHost": "mail.ringdom.org",
      "imapUser": "info@ringdom.org",
      "smtpHost": "mail.ringdom.org",
      "smtpUser": "info@ringdom.org",
      "hasImapPassword": true,
      "hasSmtpPassword": true
    }
  ],
  "validation": { "ok": true, "errors": [] }
}
```

Implementation: `loadCrmChannels()` + `validateCrmChannels()` from `features/email-crm/pipeline/imap/config.ts`.

---

## Admin — threads

### `GET /api/admin/email/threads`

List conversation threads.

| Query | Type | Description |
|-------|------|-------------|
| `status` | string | Filter: `new`, `ongoing`, `waiting`, `resolved`, or omit for all |
| `sourceChannel` | string | Filter by channel id/name (multi-mailbox) |

**Response:** `{ threads: EmailThreadRecord[] }`

### `PATCH /api/admin/email/threads`

Update thread status.

**Body:** `{ "id": "", "status": "resolved" }`

### `GET /api/admin/email/threads/[id]`

Thread detail with messages, drafts, and open tasks.

**Response:** `{ thread, messages, drafts, tasks }`

---

## Admin — drafts

### `GET /api/admin/email/drafts`

Pending drafts (`status: pending`), newest first.

### `POST /api/admin/email/drafts/[id]/approve`

Approve draft for sending. Uses session user id as reviewer.

### `POST /api/admin/email/drafts/[id]/reject`

**Body:** `{ "reason": "optional string" }`

### `POST /api/admin/email/drafts/[id]/send`

Send approved draft via **CRM channel SMTP** (`EmailSenderService`).

**Body (optional):** `{ "toEmail": "...", "subject": "..." }` — defaults from thread.

**Response:** `{ "success": true, "messageId": "" }`

Persists outbound row in `email_messages` and updates thread status to `waiting`.

---

## Admin — contacts

### `GET /api/admin/email/contacts`

| Query | Description |
|-------|-------------|
| `email`, `name`, `company`, `type` | Search filters |

### `POST /api/admin/email/contacts`

**Body:** `{ "email": "required", "name?", "company?", "type?" }`

---

## Admin — tasks

### `GET /api/admin/email/tasks`

| Query | Description |
|-------|-------------|
| `status` | `open`, `in_progress`, `overdue`, `completed`, etc. |

### `POST /api/admin/email/tasks`

**Body:** `{ "threadId", "title", "taskType", ... }` — see `TaskCreateInput` in `task-service.ts`.

### `POST /api/admin/email/tasks/[id]/complete`

**Body:** `{ "completionNotes?": "string" }`

---

## Admin — analytics

### `GET /api/admin/email/analytics`

| Query | Values |
|-------|--------|
| `range` | `7d` (default), `30d`, `90d` |

**Response:** intent/sentiment distributions, `costStats` from `email_api_usage`, `dailyStats`, draft/task summaries.

---

## Cron — email processor

### `POST` or `GET` `/api/cron/email-processor`

**Auth:** `Authorization: Bearer $CRON_SECRET` (fail-closed)

**Body or query `action`:**

| Action | Behavior |
|--------|----------|
| `poll` (default) | `pollInboundBatch()` — fetch UNSEEN per channel, process, disconnect |
| `status` | Processor + IMAP stats (`getEmailProcessor()` for this action) |
| `stop` | Stop IDLE listener |
| `start` | Start IDLE (requires `EMAIL_PROCESSOR_ALLOW_HTTP_START=true`) |
| `mark-overdue-tasks` | Run `EmailTaskService.processOverdueTasks()` |

Processor instances are constructed **per action** in the route handler (not a single top-level always-on instance).

**Example:**

```bash
curl -X POST "$BASE_URL/api/cron/email-processor" \
  -H "Authorization: Bearer $CRON_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"action":"poll"}'
```

### `GET /api/cron/email-analytics`

Returns 7-day dashboard snapshot (same shape as admin analytics). Cron-auth only.

### Auth token cleanup (related)

`GET/POST /api/cron/cleanup-email-tokens` — purges expired `email_login_tokens`. Same fail-closed `CRON_SECRET` pattern. Documented under [Ring Mailer](/docs/features/ring-mailer.md); not an Email CRM route.

---

## Webhook — inbound email

### `POST /api/webhooks/email/inbound`

**Auth:** `Authorization: Bearer $WEBHOOK_EMAIL_SECRET` **or** HMAC-SHA256 hex in `X-Email-Webhook-Signature` over raw body.

**Body (JSON):**

```json
{
  "messageId": "",
  "from": "sender@example.com",
  "fromName": "Optional Name",
  "to": "info@example.com",
  "subject": "Subject line",
  "bodyText": "Plain text body",
  "bodyHtml": "optional",
  "date": "2026-06-10T12:00:00.000Z",
  "inReplyTo": "",
  "references": ["", ""]
}
```

Calls `EmailProcessor.ingestEvent()` with `uid: 0` (no IMAP mark-seen).

---

## Related documentation

  
- [features/email-ai-crm](/docs/features/email-ai-crm.md) — Prerequisite: operator setup, channel secrets, Auth vs CRM SMTP.

  
- [architecture/email-ai-crm](/docs/architecture/email-ai-crm.md) — Deep-dive: EmailProcessor and jsonb-collection persistence.

  
- [examples/email-ai-crm](/docs/examples/email-ai-crm.md) — Same-workflow: local poll + draft send smoke test.

  
- [features/ring-mailer](/docs/features/ring-mailer.md) — See-also: Auth cleanup-email-tokens cron and SMTP_* plane.

  
- [features/owner-project-lab](/docs/features/owner-project-lab.md) — See-also: CRM orders desk sharing the admin shell.
