Transactional Templates
Designing and implementing transactional email templates for automated notifications
You are an expert in transactional email template design and implementation for automated system-generated notifications.
## Key Points
1. **Account lifecycle**: Welcome, email verification, password reset, account deactivation
2. **Commerce**: Order confirmation, payment receipt, shipping notification, delivery confirmation, refund processed
3. **Security**: Login from new device, two-factor code, password changed, suspicious activity
4. **Notifications**: Comment reply, mention, invitation, shared document
5. **Billing**: Invoice, payment failed, subscription renewal, trial expiration
- Every transactional email should have exactly one primary call to action. Secondary actions (help links, account settings) should be visually subordinate.
- Include all essential information directly in the email. Do not force users to click through to see their order total, tracking number, or other key data.
- Add security context to sensitive emails (password reset, login alerts): IP address, approximate location, device/browser. This helps users detect unauthorized access.
- Use time-limited tokens for action URLs. Display the expiration clearly ("This link expires in 30 minutes").
- Always include a plain-text version. It improves deliverability and serves users on text-only clients.
- Send transactional emails from a separate subdomain and IP from marketing emails (e.g., `notifications@mail.example.com`) to isolate deliverability reputation.
- Include an "If you didn't request this" message on security-related emails.skilldb get email-template-skills/Transactional TemplatesFull skill: 379 linesTransactional Email Templates — Email Templates
You are an expert in transactional email template design and implementation for automated system-generated notifications.
Core Philosophy
Overview
Transactional emails are triggered by user actions or system events: welcome messages, password resets, order confirmations, shipping notifications, invoices, and security alerts. Unlike marketing emails, they are expected by the recipient, have high open rates, and must be clear, timely, and functional. Their design prioritizes scannability, trust, and a single call to action.
Core Concepts
Transactional vs. Marketing Emails
| Aspect | Transactional | Marketing |
|---|---|---|
| Trigger | User action or system event | Scheduled campaign |
| Consent | Implied by account relationship | Requires explicit opt-in |
| Unsubscribe | Not required (but recommended for non-critical) | Legally required (CAN-SPAM, GDPR) |
| Priority | High — user expects it | Variable |
| Design | Functional, scannable | Brand-forward, visual |
| Personalization | Specific data (order ID, amount) | Segment-level targeting |
Common Transactional Email Types
- Account lifecycle: Welcome, email verification, password reset, account deactivation
- Commerce: Order confirmation, payment receipt, shipping notification, delivery confirmation, refund processed
- Security: Login from new device, two-factor code, password changed, suspicious activity
- Notifications: Comment reply, mention, invitation, shared document
- Billing: Invoice, payment failed, subscription renewal, trial expiration
Template Data Architecture
Define a clear contract between your application and templates:
// Type-safe template data contracts
interface WelcomeEmailData {
username: string;
verificationUrl: string;
expiresInHours: number;
}
interface OrderConfirmationData {
orderNumber: string;
orderDate: string;
items: Array<{
name: string;
quantity: number;
unitPrice: number;
imageUrl: string;
}>;
subtotal: number;
shipping: number;
tax: number;
total: number;
shippingAddress: {
name: string;
line1: string;
line2?: string;
city: string;
state: string;
zip: string;
country: string;
};
trackingUrl?: string;
}
interface PasswordResetData {
resetUrl: string;
expiresInMinutes: number;
ipAddress: string;
userAgent: string;
}
Implementation Patterns
Password Reset Template
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reset Your Password</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f6f9fc; font-family: Helvetica, Arial, sans-serif;">
<div style="display: none; max-height: 0; overflow: hidden;">
Reset your password — this link expires in {{expiresInMinutes}} minutes.
</div>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="padding: 40px 16px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0"
width="600" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px;">
<!-- Logo -->
<tr>
<td style="padding: 32px 40px 0;">
<img src="{{logoUrl}}" alt="Company" width="120" height="32" style="display: block; border: 0;" />
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 32px 40px;">
<h1 style="margin: 0 0 16px; font-size: 22px; color: #1a1a1a;">Reset your password</h1>
<p style="margin: 0 0 24px; font-size: 16px; line-height: 24px; color: #525f7f;">
We received a request to reset your password. Click the button below to choose a new one. This link expires in <strong>{{expiresInMinutes}} minutes</strong>.
</p>
<!-- Bulletproof button -->
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="border-radius: 4px; background-color: #5469d4;">
<a href="{{resetUrl}}"
style="display: inline-block; padding: 14px 32px; color: #ffffff; font-size: 16px; font-weight: bold; text-decoration: none; border-radius: 4px;">
Reset Password
</a>
</td>
</tr>
</table>
<p style="margin: 24px 0 0; font-size: 14px; line-height: 22px; color: #8898aa;">
If you did not request this, you can safely ignore this email. Your password will remain unchanged.
</p>
</td>
</tr>
<!-- Security info -->
<tr>
<td style="padding: 0 40px 32px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"
style="background-color: #f8f9fa; border-radius: 4px;">
<tr>
<td style="padding: 16px;">
<p style="margin: 0; font-size: 12px; line-height: 18px; color: #8898aa;">
This request came from IP <strong>{{ipAddress}}</strong> using {{userAgent}}.
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 24px 40px; border-top: 1px solid #e6ebf1;">
<p style="margin: 0; font-size: 12px; line-height: 18px; color: #8898aa;">
© 2026 Company Inc. · <a href="{{privacyUrl}}" style="color: #8898aa;">Privacy</a> · <a href="{{supportUrl}}" style="color: #8898aa;">Support</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
Order Confirmation Template (with Handlebars)
<table role="presentation" cellpadding="0" cellspacing="0" border="0"
width="600" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
<!-- Header -->
<tr>
<td style="padding: 32px 40px; background-color: #22c55e; color: #ffffff;">
<h1 style="margin: 0; font-size: 22px;">Order Confirmed ✓</h1>
<p style="margin: 8px 0 0; font-size: 14px; opacity: 0.9;">
Order #{{orderNumber}} · {{orderDate}}
</p>
</td>
</tr>
<!-- Items -->
<tr>
<td style="padding: 24px 40px;">
{{#each items}}
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"
style="margin-bottom: 16px;">
<tr>
<td width="80" style="vertical-align: top; padding-right: 16px;">
<img src="{{this.imageUrl}}" alt="{{this.name}}" width="80" height="80"
style="display: block; border: 0; border-radius: 4px;" />
</td>
<td style="vertical-align: top;">
<p style="margin: 0; font-size: 16px; font-weight: bold; color: #1a1a1a;">{{this.name}}</p>
<p style="margin: 4px 0 0; font-size: 14px; color: #525f7f;">
Qty: {{this.quantity}} · ${{this.unitPrice}} each
</p>
</td>
</tr>
</table>
{{/each}}
</td>
</tr>
<!-- Totals -->
<tr>
<td style="padding: 0 40px 24px;">
<table role="table" cellpadding="0" cellspacing="0" border="0" width="100%"
style="border-top: 1px solid #e6ebf1;">
<tr>
<td style="padding: 12px 0; font-size: 14px; color: #525f7f;">Subtotal</td>
<td style="padding: 12px 0; font-size: 14px; color: #525f7f; text-align: right;">${{subtotal}}</td>
</tr>
<tr>
<td style="padding: 4px 0; font-size: 14px; color: #525f7f;">Shipping</td>
<td style="padding: 4px 0; font-size: 14px; color: #525f7f; text-align: right;">${{shipping}}</td>
</tr>
<tr>
<td style="padding: 4px 0; font-size: 14px; color: #525f7f;">Tax</td>
<td style="padding: 4px 0; font-size: 14px; color: #525f7f; text-align: right;">${{tax}}</td>
</tr>
<tr>
<td style="padding: 12px 0; font-size: 16px; font-weight: bold; color: #1a1a1a; border-top: 1px solid #e6ebf1;">Total</td>
<td style="padding: 12px 0; font-size: 16px; font-weight: bold; color: #1a1a1a; text-align: right; border-top: 1px solid #e6ebf1;">${{total}}</td>
</tr>
</table>
</td>
</tr>
<!-- Shipping address -->
<tr>
<td style="padding: 0 40px 32px;">
<h2 style="margin: 0 0 8px; font-size: 14px; text-transform: uppercase; color: #8898aa; letter-spacing: 1px;">
Shipping To
</h2>
<p style="margin: 0; font-size: 14px; line-height: 22px; color: #525f7f;">
{{shippingAddress.name}}<br />
{{shippingAddress.line1}}<br />
{{#if shippingAddress.line2}}{{shippingAddress.line2}}<br />{{/if}}
{{shippingAddress.city}}, {{shippingAddress.state}} {{shippingAddress.zip}}<br />
{{shippingAddress.country}}
</p>
</td>
</tr>
</table>
Template Rendering Pipeline
import Handlebars from "handlebars";
import juice from "juice";
import { minify } from "html-minifier-terser";
import fs from "fs/promises";
// Register helpers
Handlebars.registerHelper("currency", (value: number) =>
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(value)
);
Handlebars.registerHelper("dateFormat", (date: string) =>
new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
);
interface RenderOptions {
templateName: string;
data: Record<string, unknown>;
inlineCSS?: boolean;
}
async function renderEmail({ templateName, data, inlineCSS = true }: RenderOptions) {
const source = await fs.readFile(`./templates/${templateName}.html`, "utf8");
const template = Handlebars.compile(source);
let html = template(data);
if (inlineCSS) {
html = juice(html);
}
html = await minify(html, {
collapseWhitespace: true,
removeComments: true,
minifyCSS: true,
});
return html;
}
// Usage
const html = await renderEmail({
templateName: "order-confirmation",
data: {
orderNumber: "ORD-98765",
orderDate: "2026-03-15",
items: [
{ name: "Wireless Headphones", quantity: 1, unitPrice: 79.99, imageUrl: "..." },
],
subtotal: 79.99,
shipping: 5.99,
tax: 6.88,
total: 92.86,
shippingAddress: {
name: "Alice Johnson",
line1: "123 Main St",
city: "Portland",
state: "OR",
zip: "97201",
country: "United States",
},
},
});
Multi-Provider Sending Abstraction
interface EmailProvider {
send(params: {
to: string;
from: string;
subject: string;
html: string;
text: string;
}): Promise<{ id: string }>;
}
class EmailService {
private primary: EmailProvider;
private fallback: EmailProvider;
constructor(primary: EmailProvider, fallback: EmailProvider) {
this.primary = primary;
this.fallback = fallback;
}
async send(params: { to: string; from: string; subject: string; html: string; text: string }) {
try {
return await this.primary.send(params);
} catch (err) {
console.error("Primary provider failed, falling back:", err);
return await this.fallback.send(params);
}
}
}
Best Practices
- Every transactional email should have exactly one primary call to action. Secondary actions (help links, account settings) should be visually subordinate.
- Include all essential information directly in the email. Do not force users to click through to see their order total, tracking number, or other key data.
- Add security context to sensitive emails (password reset, login alerts): IP address, approximate location, device/browser. This helps users detect unauthorized access.
- Use time-limited tokens for action URLs. Display the expiration clearly ("This link expires in 30 minutes").
- Always include a plain-text version. It improves deliverability and serves users on text-only clients.
- Send transactional emails from a separate subdomain and IP from marketing emails (e.g.,
notifications@mail.example.com) to isolate deliverability reputation. - Include an "If you didn't request this" message on security-related emails.
- Log every transactional email sent, including template name, recipient, timestamp, and provider response ID for debugging delivery issues.
Common Pitfalls
- Mixing marketing content into transactional emails: Adding promotional banners to a password reset email violates CAN-SPAM regulations and erodes trust. Keep transactional emails purely functional.
- Missing plain-text fallback: Sending HTML-only emails hurts deliverability scores with major providers and fails for text-only clients.
- Tokens that never expire: Password reset and verification tokens should have a short TTL (15-60 minutes). Long-lived tokens are a security risk.
- Hardcoded sender addresses: Use environment configuration for from addresses so staging and production environments do not collide.
- No idempotency guard: If a user clicks "resend verification" multiple times, ensure you do not create duplicate tokens or send excessive emails. Debounce or rate-limit.
- Unhandled template variables: A missing variable in a Handlebars/Liquid template renders as an empty string, producing broken sentences. Validate data completeness before rendering.
- Not testing with blocked images: Many corporate clients block images by default. Verify that the email is understandable with all images replaced by their alt text.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add email-template-skills
Related Skills
Dark Mode Email
Dark mode support patterns for email templates across major email clients
Email Accessibility
Accessible email design patterns for inclusive, standards-compliant email templates
Email Deliverability
Email deliverability essentials including SPF, DKIM, DMARC, and inbox placement
Email Testing
Email testing workflows using Litmus, Email on Acid, Mailtrap, and other QA tools
Mjml
Building responsive email templates with the MJML markup language and toolchain
React Email
Building email templates with React Email components and rendering pipeline