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.

Rewrite this repo to API v2
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, 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_....

v1v2
POST /api/v1/emailsPOST /api/v2/emails
POST /api/v1/emails/verifyPOST /api/v2/emails/verify
POST /api/v1/subscribersPOST /api/v2/subscribers
GET /api/v1/subscribersGET /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}/unsubscribePOST /api/v2/subscribers/{subscriber}/unsubscribe
POST /api/v1/subscribers/{subscriber}/tagsPOST /api/v2/subscribers/{subscriber}/tags
DELETE /api/v1/subscribers/{subscriber}/tagsDELETE /api/v2/subscribers/{subscriber}/tags
GET /api/v1/subscribers/{subscriber}/eventsGET /api/v2/subscribers/{subscriber}/events
POST /api/v1/eventsPOST /api/v2/events
GET /api/v1/tagsGET /api/v2/tags
POST /api/v1/tagsPOST /api/v2/tags
GET /api/v1/tags/{tag}GET /api/v2/tags/{tag}
GET /api/v1/campaignsGET /api/v2/campaigns
POST /api/v1/campaignsPOST /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}/sendPOST /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_..." }.

v1v2
content + omitted contentType or MARKDOWNmarkdown
content + contentType: "HTML"html
content + contentType: "TIPTAP"tiptap
to stringto string or one-address array
replyToreply_to
trackingdrop it
previewpreview (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.

v1v2
{ success: true, id, message } on send{ id }
{ success: true, subscriber } on create/getthe 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 queryafter / before

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

TypeScript SDK

[email protected] talks only to v2 and returns { data, error }.

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