Send React Email Templates with Lumail
Build transactional email templates as React components, render them to HTML, and send them through the Lumail transactional email API.
React Email lets you write email templates as React components and render them to email-safe HTML. Lumail accepts that HTML directly through the transactional email API with contentType: "HTML", so React Email covers the template layer and Lumail covers sending, domain authentication, subscriber records, and delivery tracking.
When to Use This
Use React Email with Lumail for transactional email: receipts, password resets, magic links, invitations, order confirmations, and product notifications.
Campaigns are a different content pipeline: they accept MARKDOWN and MAILY, not HTML. Newsletters and broadcasts stay in Lumail campaigns and workflows so unsubscribes, segmentation, and campaign reporting remain correct.
Prerequisites
- A verified Lumail email domain, so your
fromaddress is allowed. - A Lumail API token (starts with
lum_). See Create API Token. - A Node.js project that can render React on the server.
Step 1 - Install the Packages
pnpm add lumail @react-email/components @react-email/render
@react-email/components provides the email-safe primitives. @react-email/render turns a component into an HTML string. lumail is the TypeScript SDK.
To preview templates locally while you build them, add the React Email dev server:
pnpm add -D react-email
Step 2 - Write the Template
Create emails/order-confirmation.tsx:
import {
Body,
Button,
Container,
Head,
Heading,
Hr,
Html,
Link,
Preview,
Section,
Text,
} from "@react-email/components";
type OrderConfirmationProps = {
name: string;
orderNumber: string;
total: string;
receiptUrl: string;
};
export default function OrderConfirmation({
name,
orderNumber,
total,
receiptUrl,
}: OrderConfirmationProps) {
return (
<Html>
<Head />
<Preview>Your order {orderNumber} is confirmed</Preview>
<Body
style={{ backgroundColor: "#f6f6f6", fontFamily: "Arial, sans-serif" }}
>
<Container
style={{
maxWidth: "600px",
padding: "24px",
backgroundColor: "#ffffff",
}}
>
<Heading style={{ fontSize: "22px", color: "#111111" }}>
Thanks for your order, {name}
</Heading>
<Text style={{ fontSize: "15px", color: "#333333" }}>
Order <strong>{orderNumber}</strong> is confirmed. Total: {total}.
</Text>
<Section style={{ margin: "24px 0" }}>
<Button
href={receiptUrl}
style={{
backgroundColor: "#111111",
color: "#ffffff",
padding: "12px 20px",
borderRadius: "6px",
}}
>
View your receipt
</Button>
</Section>
<Hr />
<Text
style={{ fontSize: "12px", color: "#666666", textAlign: "center" }}
>
Questions?{" "}
<Link href="mailto:[email protected]">Contact us</Link>
</Text>
</Container>
</Body>
</Html>
);
}
Pass per-recipient data as React props. A transactional send targets one recipient, so the values are already known at render time and you keep full type safety.
Step 3 - Render to HTML
render() from @react-email/render v2 is asynchronous and returns the full HTML document.
import { render } from "@react-email/render";
import OrderConfirmation from "@/emails/order-confirmation";
const html = await render(
<OrderConfirmation
name="Jane"
orderNumber="A-1042"
total="$99.00"
receiptUrl="https://yourdomain.com/receipts/A-1042"
/>,
);
Render in a .tsx file so JSX compiles. If your send logic lives in a .ts file, keep rendering in a separate .tsx module and import the resulting function.
Step 4 - Send Through Lumail
import { Lumail } from "lumail";
const lumail = new Lumail({ apiKey: process.env.LUMAIL_API_KEY! });
const { qstashMessageId } = await lumail.emails.send({
to: "[email protected]",
from: "[email protected]",
subject: "Your order A-1042 is confirmed",
content: html,
contentType: "HTML",
transactional: true,
});
The preview field is ignored for HTML content - only the Markdown and Tiptap pipelines inject it. The preheader comes from React Email's <Preview> component in your template.
Each recipient of a transactional send becomes a Lumail subscriber if they are not one already, and addresses on the blocklist are rejected before queueing.
The equivalent raw API call:
curl -X POST https://lumail.io/api/v1/emails \
-H "Authorization: Bearer $LUMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"from": "[email protected]",
"subject": "Your order A-1042 is confirmed",
"content": "<html>...rendered react email...</html>",
"contentType": "HTML",
"transactional": true
}'
A success response means Lumail queued the email on the transactional priority lane. See Send Transactional Email for the full field list and response shape.
Complete Example
A single server function that renders and sends:
import { render } from "@react-email/render";
import { Lumail } from "lumail";
import OrderConfirmation from "@/emails/order-confirmation";
const lumail = new Lumail({ apiKey: process.env.LUMAIL_API_KEY! });
export async function sendOrderConfirmation(order: {
email: string;
name: string;
orderNumber: string;
total: string;
receiptUrl: string;
}) {
const html = await render(
<OrderConfirmation
name={order.name}
orderNumber={order.orderNumber}
total={order.total}
receiptUrl={order.receiptUrl}
/>,
);
return lumail.emails.send({
to: order.email,
from: "[email protected]",
subject: `Your order ${order.orderNumber} is confirmed`,
content: html,
contentType: "HTML",
transactional: true,
});
}
React Props vs Lumail Variables
Lumail replaces {{variable}} placeholders in HTML content before sending. Both mechanisms work, and they are good at different things.
| Data | Use |
|---|---|
| Order totals, names, URLs, anything your app already has | React props |
{{unsubscribeUrl}}, {{onlineUrl}} | Lumail variables |
| Lumail custom subscriber fields | Lumail variables |
Lumail owns the unsubscribe and view-in-browser URLs, so those must stay as placeholders.
Two limits are worth knowing before you rely on placeholders in a React Email template:
- Placeholder names must be word characters. Hyphenated organization variables such as
{{org-name}}are not substituted inHTMLcontent and are delivered literally. Pass organization values as React props instead. {{unsubscribeUrl}}resolves to an empty string on transactional sends, because a transactional email has no unsubscribe link. It is only meaningful whentransactionalis false.
See Send Email in HTML for the full variable list.
Escaping Braces in JSX
{{unsubscribeUrl}} inside JSX is parsed as a JavaScript expression containing an object literal, not as text. Always pass it as a string:
// Correct
<Link href={"{{unsubscribeUrl}}"}>Unsubscribe</Link>
<Text>Hello {"{{name}}"}</Text>
// Broken - JSX evaluates the braces
<Link href={{unsubscribeUrl}}>Unsubscribe</Link>
What Lumail Does to Your HTML
Your rendered document is sent through mostly untouched. Lumail applies these transformations:
- Variable replacement -
{{...}}placeholders are substituted with subscriber values. - Open tracking - a 1x1 pixel is inserted just before
</body>when open tracking is on. React Email always emits a</body>, so the pixel lands inside the document. - Unsubscribe fallback - for non-transactional sends only, if the HTML contains no
{{unsubscribeUrl}}, Lumail appends an unsubscribe block to the end of the string, after your closing</html>. Include{{unsubscribeUrl}}in your template to control where it renders. - Plain text version - generated by stripping HTML tags from your content.
- Click tracking - link rewriting for
HTMLcontent targets Markdown-style[text](url)links, so<a href>URLs in a React Email template are delivered unchanged. Your original links stay intact, which is usually what you want for password resets and magic links.
Set tracking.open: false if you do not want the pixel at all.
Notes on Styling
- Inline styles are the safest option and are what the React Email primitives use by default.
<Tailwind>works, since it inlines utility classes at render time. Responsive variants and pseudo-classes still fall back to a<style>block in<head>, which many clients ignore.- Keep the body width at or below 600px.
- The auto-generated plain text version comes from a tag strip, so text inside
<style>blocks can leak into it. Prefer inline styles and keep<head>CSS minimal. - Preview across clients before shipping a new template.
Preview Templates Locally
With react-email installed as a dev dependency, add a script:
{
"scripts": {
"email": "email dev --dir ./emails"
}
}
Then run pnpm email and open the local preview to iterate on the template before wiring up the send.
Related
- Send Transactional Email - full endpoint reference
- Send Email in HTML - variables and email-safe HTML rules
- SDK Resources - other SDK operations
- Send Transactional Email with SMTP - for tools that cannot call the API