v1 HTTP is frozen. New work uses `/api/v2`. Same `lum_` Bearer token. Same organization.

If you are an agent rewriting code, follow this page in order. Do not invent fields. Do not keep `content` / `contentType`. Do not wrap v2 JSON in `success: true`.

<AgentPrompt
  label="Rewrite this repo to API v2"
  prompt="Read https://lumail.io/docs/api-reference/v2/migration.md then https://lumail.io/openapi.json. Replace every Lumail /api/v1 call with the v2 equivalent on this page. Rewrite send bodies to exactly one of html, tiptap, or markdown. Parse v2 JSON as documented (no success wrapper). Upgrade lumail to 2.1.0 if the TypeScript SDK is used and handle { data, error } results. Leave SMTP, /api/v1/integrations/*, /api/v1/admin/tools, MCP, and the CLI tools runner unchanged. Show the diff and a smoke-test command. Do not send a campaign."
/>

## Do this first

1. Find every `lumail.io/api/v1`, `https://api.lumail.io/v1`, and `lumail.emails.send({ content`.
2. Classify each hit: REST send, REST other, TypeScript SDK, SMTP, webhook URL, MCP/CLI.
3. Rewrite REST and SDK only. Stop if the hit is SMTP, `/api/v1/integrations/`, or `/api/v1/admin/tools`.
4. After the rewrite, send one test email with [Send Email](/docs/api-reference/v2/emails-send), then [Get Email](/docs/api-reference/v2/emails-get) by the returned `id`.

Machine-readable contract: [openapi.json](https://lumail.io/openapi.json). Endpoint catalog: [API v2](/docs/api-reference/v2). SDK: [`lumail@2.1.0`](/docs/sdk/v2/introduction).

## Path map

Prefix every path with `https://lumail.io`. Auth stays `Authorization: Bearer lum_...`.

| v1 | v2 |
| -- | -- |
| `POST /api/v1/emails` | `POST /api/v2/emails` |
| `POST /api/v1/emails/verify` | `POST /api/v2/emails/verify` |
| `POST /api/v1/subscribers` | `POST /api/v2/subscribers` |
| `GET /api/v1/subscribers` | `GET /api/v2/subscribers` |
| `GET /api/v1/subscribers/{subscriber}` | `GET /api/v2/subscribers/{subscriber}` |
| `PATCH /api/v1/subscribers/{subscriber}` | `PATCH /api/v2/subscribers/{subscriber}` |
| `POST /api/v1/subscribers/{subscriber}/unsubscribe` | `POST /api/v2/subscribers/{subscriber}/unsubscribe` |
| `POST /api/v1/subscribers/{subscriber}/tags` | `POST /api/v2/subscribers/{subscriber}/tags` |
| `DELETE /api/v1/subscribers/{subscriber}/tags` | `DELETE /api/v2/subscribers/{subscriber}/tags` |
| `GET /api/v1/subscribers/{subscriber}/events` | `GET /api/v2/subscribers/{subscriber}/events` |
| `POST /api/v1/events` | `POST /api/v2/events` |
| `GET /api/v1/tags` | `GET /api/v2/tags` |
| `POST /api/v1/tags` | `POST /api/v2/tags` |
| `GET /api/v1/tags/{tag}` | `GET /api/v2/tags/{tag}` |
| `GET /api/v1/campaigns` | `GET /api/v2/campaigns` |
| `POST /api/v1/campaigns` | `POST /api/v2/campaigns` |
| `GET /api/v1/campaigns/{campaignId}` | `GET /api/v2/campaigns/{campaignId}` |
| `PATCH /api/v1/campaigns/{campaignId}` | `PATCH /api/v2/campaigns/{campaignId}` |
| `DELETE /api/v1/campaigns/{campaignId}` | `DELETE /api/v2/campaigns/{campaignId}` |
| `POST /api/v1/campaigns/{campaignId}/send` | `POST /api/v2/campaigns/{campaignId}/send` |

v2-only (no v1 equivalent): `GET /api/v2/emails`, `GET /api/v2/emails/{id}`, `POST /api/v2/emails/batch`, domains (`/api/v2/domains`), tokens (`/api/v2/tokens`), tools (`/api/v2/tools`).

Leave unchanged: `smtp.lumail.io`, `POST /api/v1/integrations/*`, `GET|POST /api/v1/admin/tools`, `https://lumail.io/mcp`, `lumail tools run`.

## Send email (the breaking change)

v1 body used `content` + optional `contentType` (`MARKDOWN` default, `HTML`, `TIPTAP`) and returned `{ success, message, id }`.

v2 requires exactly one of `html`, `tiptap`, or `markdown`. Response is `{ "id": "eml_..." }`.

| v1 | v2 |
| -- | -- |
| `content` + omitted `contentType` or `MARKDOWN` | `markdown` |
| `content` + `contentType: "HTML"` | `html` |
| `content` + `contentType: "TIPTAP"` | `tiptap` |
| `to` string | `to` string or one-address array |
| `replyTo` | `reply_to` |
| `tracking` | drop it |
| `preview` | `preview` (unchanged) |
| `{ success, message, id }` | `{ id }` |

Drop these if present: `content`, `contentType`, `attachments`, `template`, `topic_id`, `scheduled_at`, `react`.

```javascript
// v1
await fetch("https://lumail.io/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "hello@yourdomain.com",
    to: "user@example.com",
    subject: "Welcome",
    content: "# Hello {{name}}",
    contentType: "MARKDOWN",
  }),
});

// v2
const response = await fetch("https://lumail.io/api/v2/emails", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "hello@yourdomain.com",
    to: "user@example.com",
    subject: "Welcome",
    markdown: "Hello **there**.",
  }),
});
const { id } = await response.json();
```

After send, poll or fetch `GET /api/v2/emails/{id}` for `last_event`, `html`, and `text`. List (`GET /api/v2/emails`) does not return bodies.

Every v2 error is `{ name, message, statusCode }` plus optional `details`. `429` includes `Retry-After`.

## Responses and lists

Do not read `success` wrappers from v2 JSON.

| v1 | v2 |
| -- | -- |
| `{ success: true, id, message }` on send | `{ id }` |
| `{ success: true, subscriber }` on create/get | the subscriber object |
| `{ success: true, subscribers, total }` on list | `{ object: "list", data, has_more }` with `limit` / `after` / `before` |
| `{ success: true, campaign, campaignId }` | the campaign object |
| `page` or `cursor` query | `after` / `before` |

`GET /api/v2/subscribers` query: `limit`, `after`, `status`, `tag`, `query`.

## TypeScript SDK

`lumail@2.1.0` talks only to v2 and returns `{ data, error }`.

```bash
pnpm add lumail@2.1.0
```

```typescript
import { Lumail } from "lumail";

const lumail = new Lumail({ apiKey: process.env.LUMAIL_API_KEY });

const { data, error } = await lumail.emails.send(
  {
    from: "hello@yourdomain.com",
    to: "user@example.com",
    subject: "Welcome",
    html: "<p>Hello</p>",
  },
  { idempotencyKey: "welcome-1" },
);
if (error) throw new Error(error.message);
const id = data.id;
```

Replace `emails.send({ content, contentType, replyTo, tracking })` with `html` | `markdown` | `tiptap` and `reply_to`. See [SDK errors](/docs/sdk/v2/errors).

MCP, ChatGPT, Cursor, and `lumail tools run` already call the live tool catalog. Do not version those URLs.

## Checklist

- No remaining `/api/v1/` in application code except integrations and admin tools
- No `content` / `contentType` on send
- Send response handled as `{ id }`
- `lumail` dependency is `2.1.0` if the SDK is used
- One test send returned an `eml_` id and Get Email returns that row
- No campaign send as part of the migration

## Related

- [Send Email](/docs/api-reference/v2/emails-send)
- [Get Email](/docs/api-reference/v2/emails-get)
- [Create Subscriber](/docs/api-reference/v2/subscribers-create)
- [SDK 2.1.0](/docs/sdk/v2/introduction)
- [v1 API reference](/docs/api-reference) (frozen)
