Migration to V2
Rewrite a Lumail v1 integration to API v2. Instructions for coding agents.
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.
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
- Find every
lumail.io/api/v1,https://api.lumail.io/v1, andlumail.emails.send({ content. - Classify each hit: REST send, REST other, TypeScript SDK, SMTP, webhook URL, MCP/CLI.
- Rewrite REST and SDK only. Stop if the hit is SMTP,
/api/v1/integrations/, or/api/v1/admin/tools. - After the rewrite, send one test email with Send Email, then Get Email by the returned
id.
Machine-readable contract: openapi.json. Endpoint catalog: API v2. SDK: [email protected].
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.
// 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: "[email protected]",
to: "[email protected]",
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: "[email protected]",
to: "[email protected]",
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
[email protected] talks only to v2 and returns { data, error }.
pnpm add [email protected]import { Lumail } from "lumail";
const lumail = new Lumail({ apiKey: process.env.LUMAIL_API_KEY });
const { data, error } = await lumail.emails.send(
{
from: "[email protected]",
to: "[email protected]",
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.
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/contentTypeon send - Send response handled as
{ id } lumaildependency is2.1.0if 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