Traditional SaaS bills by seat. AI products bill by tokens, generations, runs, credits, or compute seconds. That single change rewrites the whole email taxonomy. Welcome and password reset still matter, but the emails that decide whether a user keeps their card on file are the ones that fire during usage: the receipt after a generation, the warning at 90% of monthly credits, the alert when an API key starts burning through quota at 3am.
Most teams shipping AI products in 2026 reuse a generic transactional stack and end up with two problems. Users hit a usage wall with no warning, then churn. Or every action triggers a noisy email and users filter the sender into spam. The fix is a smaller, AI-specific set of templates with thresholds that match how the billing actually works.
- AI SaaS apps need 7-8 email types that traditional SaaS doesn't: generation receipts, usage warnings, quota resets, model updates, API key events, rate-limit alerts, billing thresholds, and fine-tune completion.
- Threshold emails should fire at 70%, 90%, and 100% of the metered limit, not just at the wall.
- Every billed action gets a receipt, but receipts should be daily/weekly digests, not per-action, unless the unit cost is high (fine-tunes, video generations, agent runs).
- Security-critical events (new API key, key rotation, key revoked) need an immediate, non-batched email for compliance and account takeover detection.
The AI SaaS email taxonomy at a glance
Before walking through individual templates, here's the full set with the trigger, intent, and what should be in the bundle for each. If you're running an AI product, this is roughly the inventory you need before launch.
| Trigger | Intent | Frequency | |
|---|---|---|---|
| Waitlist confirmed | User joins waitlist | Confirm spot, set expectation | Once per signup |
| Access granted | Beta or invite-only access opens | Convert waitlist to active user | Once per user |
| Generation receipt | High-cost run completes (fine-tune, video, agent) | Confirm spend, prove value | Per high-cost action |
| Usage warning | 70% / 90% of credits used | Pre-empt block, prompt upgrade | Twice per cycle, max |
| Limit reached | 100% of credits used | Explain block, give 1-click upgrade | Once per cycle |
| Quota reset | New billing cycle starts | Re-engage, surface what they did last cycle | Once per cycle |
| API key created | New key generated | Confirm legitimate creation, anti-fraud | Per event |
| Model or feature update | New model rolled out, plan unlocks feature | Drive re-engagement, surface value | Per release |
| Rate limit warning | Sustained 429s on an API key | Help debug, prevent runaway costs | Throttled to 1/hr |
| Billing threshold | Spend hits configurable cap (e.g., $500) | Prevent overage shock | Per threshold cross |
The 10 email types most AI products end up needing. Some can be combined; none can be skipped.
Why the traditional SaaS template set falls short
A SaaS Essentials pack covers welcome, password reset, magic link, invoice, trial ending, and subscription renewal. That set assumes a model where the user pays once a month for a fixed seat, uses the product, and either renews or cancels. The user's relationship with the bill is monthly.
AI products break that assumption in four ways:
- Variable cost per session. A user can spend $0.02 on a small chat or $4 on a long agent run. The bill is no longer predictable from the plan name.
- Hard quota walls.Most credit-based plans block the user at 100%. Without warning emails, the first time a user knows they're over is when their job fails.
- Infrastructure-style events. API keys, rate limits, and webhook failures look more like AWS than like Notion. Users expect logs and alerts.
- Fast model churn.A new frontier model drops every few months. Users want to know when they unlock a better default, and competitors will tell them if you don't.
Generation receipts: the new transactional email
If a single action can cost a user $4, send a receipt. Replicate does this for every prediction. Vercel does it when a paid v0 generation completes. The email is short: cost, output reference, credits remaining, done.
For chat-style products where users fire fifty cheap actions a day, per-action receipts turn into noise and end up in spam. Batch into a daily or weekly digest summarizing usage, spend, and what was generated. The rule is roughly: if the unit cost is under a dollar, batch it.
A typed generation receipt template
Here's the shape we use in the AI SaaS bundle. The component accepts strongly typed props for the action, cost, and remaining credits, and renders a receipt that survives Outlook and dark-mode Gmail.
import {
Body, Container, Head, Heading, Html, Preview,
Section, Text, Hr, Button,
} from "@react-email/components";
export type GenerationReceiptProps = {
userName: string;
action:
| { kind: "fine-tune"; modelName: string; durationMinutes: number }
| { kind: "video"; outputSeconds: number; resolution: "720p" | "1080p" | "4k" }
| { kind: "agent-run"; stepsCompleted: number; toolsUsed: number };
creditsUsed: number;
creditsRemaining: number;
monthlyAllowance: number;
outputUrl: string;
invoiceId: string;
};
export default function GenerationReceipt(props: GenerationReceiptProps) {
const {
userName, action, creditsUsed, creditsRemaining,
monthlyAllowance, outputUrl, invoiceId,
} = props;
const headline = headlineFor(action);
const detail = detailFor(action);
const usagePercent = Math.round(
((monthlyAllowance - creditsRemaining) / monthlyAllowance) * 100
);
return (
<Html>
<Head />
<Preview>{`${headline} - ${creditsUsed} credits`}</Preview>
<Body style={{ backgroundColor: "#f6f8fa", fontFamily: "system-ui, sans-serif" }}>
<Container style={{ maxWidth: 560, padding: "32px 24px" }}>
<Heading as="h1" style={{ fontSize: 20, color: "#0a0a0a" }}>
{headline}
</Heading>
<Text style={{ color: "#52525b" }}>Hi {userName},</Text>
<Text style={{ color: "#52525b" }}>
{detail} Receipt below for your records.
</Text>
<Section style={card}>
<Row label="Credits used" value={`${creditsUsed.toLocaleString()}`} />
<Row label="Credits remaining" value={`${creditsRemaining.toLocaleString()}`} />
<Row label="Monthly usage" value={`${usagePercent}%`} />
<Row label="Receipt" value={invoiceId} mono />
</Section>
<Button href={outputUrl} style={button}>
View output
</Button>
<Hr style={{ borderColor: "#e4e4e7", margin: "32px 0 16px" }} />
<Text style={{ color: "#71717a", fontSize: 12 }}>
Receipts can be muted from Settings → Notifications.
</Text>
</Container>
</Body>
</Html>
);
}
function headlineFor(action: GenerationReceiptProps["action"]) {
if (action.kind === "fine-tune") return `Fine-tune complete: ${action.modelName}`;
if (action.kind === "video") return `Video ready (${action.outputSeconds}s, ${action.resolution})`;
return `Agent run complete (${action.stepsCompleted} steps)`;
}
function detailFor(action: GenerationReceiptProps["action"]) {
if (action.kind === "fine-tune") {
return `Your fine-tune for ${action.modelName} finished after ${action.durationMinutes} minutes.`;
}
if (action.kind === "video") {
return `Your ${action.outputSeconds}-second ${action.resolution} clip is rendered and ready.`;
}
return `Your agent finished after ${action.stepsCompleted} steps using ${action.toolsUsed} tools.`;
}The discriminated union on actionmatters more than it looks. The receipt copy can't drift out of sync with the action type, and adding a new generation type becomes a TypeScript-enforced refactor instead of a fragile string match. The footer also points users to mute receipts in settings rather than a hard unsubscribe; receipts are transactional, but if you don't give power users an opt-out, they'll mark you as spam, which costs you on the deliverability side.
Usage and quota warning emails
The single highest-leverage email an AI product sends is the one that fires before a user hits their quota. The math is straightforward: if a user hits 100% with no warning, the next interaction is a failed request and a frustrated rage-tweet. If they get an email at 70% with a clean upgrade path, you capture the upgrade or, at worst, prevent the churn.
The threshold pattern that holds up across products:
| Threshold | Tone | CTA |
|---|---|---|
| 70% | Informational, neutral | View usage / Upgrade plan |
| 90% | Concrete, action-oriented | Upgrade plan / Add credits |
| 100% | Explanatory, helpful | Upgrade now (one click) |
| Cycle reset | Re-engagement | Pick up where you left off |
A usage warning template
type WarningLevel = "approaching" | "near" | "reached";
export type UsageWarningProps = {
userName: string;
level: WarningLevel;
used: number;
total: number;
cycleResetsAt: string; // ISO date
upgradeUrl: string;
topUpUrl?: string;
};
const COPY: Record<WarningLevel, { subject: string; headline: string; body: string }> = {
approaching: {
subject: "You've used 70% of this month's credits",
headline: "You're 70% through this month's credits",
body:
"No action needed yet. Heads up so you can decide whether to upgrade " +
"before you run out.",
},
near: {
subject: "Only 10% of credits remaining",
headline: "10% of credits remaining",
body:
"At your current pace you'll likely hit your monthly limit in the next " +
"few days. Upgrading now keeps everything running without interruption.",
},
reached: {
subject: "Monthly credit limit reached",
headline: "You've used 100% of this month's credits",
body:
"New requests are paused until your cycle resets. You can upgrade your " +
"plan or top up credits to keep working today.",
},
};
export default function UsageWarning(props: UsageWarningProps) {
const { userName, level, used, total, cycleResetsAt, upgradeUrl, topUpUrl } = props;
const copy = COPY[level];
const percent = Math.round((used / total) * 100);
const resetDate = new Date(cycleResetsAt).toLocaleDateString(undefined, {
month: "long",
day: "numeric",
});
return (
<Html>
<Head />
<Preview>{copy.subject}</Preview>
<Body style={{ backgroundColor: "#f6f8fa", fontFamily: "system-ui" }}>
<Container style={{ maxWidth: 560, padding: "32px 24px" }}>
<Heading as="h1" style={{ fontSize: 22, color: "#0a0a0a" }}>
{copy.headline}
</Heading>
<Text>Hi {userName},</Text>
<Text style={{ color: "#52525b" }}>{copy.body}</Text>
<Section style={card}>
<UsageBar percent={percent} />
<Text style={{ marginTop: 12, color: "#52525b", fontSize: 14 }}>
{used.toLocaleString()} / {total.toLocaleString()} credits used.
Cycle resets on {resetDate}.
</Text>
</Section>
<Button href={upgradeUrl} style={button}>
Upgrade plan
</Button>
{topUpUrl && (
<Button href={topUpUrl} style={buttonSecondary}>
Add credits without upgrading
</Button>
)}
</Container>
</Body>
</Html>
);
}The piece that matters most here is the COPY map. Three thresholds, three different tones, one component. When you pull this template into your app, the dispatcher decides which level to fire and the email handles the rest.
API key and security notification emails
Every product that issues API keys eventually deals with leaked keys. A developer commits a key to a public repo, GitHub's secret scanning catches it, and the product owner needs to know within seconds - not whenever the next aggregated report goes out.
Three events deserve their own immediate email, never batched:
- API key created.Anti-fraud and account takeover detection. If a user didn't expect this email, that's a signal.
- API key revoked. Confirms a security action took effect, especially when triggered by automated scanning.
- API key rotated. Pairs with rotation deadlines enforced by the platform.
export type ApiKeyCreatedProps = {
userName: string;
keyName: string;
keyPrefix: string; // e.g., "sk-prod-abc..."
createdAt: string;
ipAddress: string;
userAgent: string;
reviewUrl: string;
revokeUrl: string;
};
export default function ApiKeyCreated(props: ApiKeyCreatedProps) {
const { userName, keyName, keyPrefix, createdAt, ipAddress, userAgent, reviewUrl, revokeUrl } = props;
const created = new Date(createdAt).toLocaleString();
return (
<Html>
<Head />
<Preview>{`New API key created: ${keyName}`}</Preview>
<Body style={{ backgroundColor: "#f6f8fa", fontFamily: "system-ui" }}>
<Container style={{ maxWidth: 560, padding: "32px 24px" }}>
<Heading as="h1" style={{ fontSize: 20, color: "#0a0a0a" }}>
New API key created
</Heading>
<Text>Hi {userName},</Text>
<Text style={{ color: "#52525b" }}>
A new API key was just created on your account.
If this wasn't you, revoke it immediately.
</Text>
<Section style={card}>
<Row label="Name" value={keyName} />
<Row label="Key" value={`${keyPrefix}...`} mono />
<Row label="Created" value={created} />
<Row label="From IP" value={ipAddress} mono />
<Row label="User-Agent" value={userAgent} />
</Section>
<Button href={reviewUrl} style={button}>
Review activity
</Button>
<Button href={revokeUrl} style={buttonDanger}>
Revoke this key
</Button>
</Container>
</Body>
</Html>
);
}Model and feature update emails
When a new model lands (GPT-5, Claude 5, your own fine-tuned variant), users want to know what changed in their stack. For products that bill by usage, this email earns opens. Better models usually mean fewer retries and lower per-task cost. Tell users before they figure it out from a benchmark.
What works:
- One sentence on what's new (model name, capability).
- A concrete before/after metric (latency dropped 30%, output quality score up 12 points).
- What the user has to do (usually nothing - default just changed) or a one-click toggle if it's opt-in.
Pattern: model rollout email
Subject: Claude 4.7 is now the default for your workspace
Body: Two paragraphs. First explains what changed (default model, opt-out path). Second shows a measurable improvement on a benchmark they'll recognize. Ends with a single CTA to try it on a recent prompt.
Skip the press-release tone. Users on these lists are technical. They skim for the model name, the change, and whether their integration breaks. Get those three things in the first 200 characters and the rest of the email can be optional reading.
Rate limit and billing threshold alerts
These two emails are operational. They're less about lifecycle and more about preventing a 3am page from a customer.
Rate limit warnings
Fire when an API key returns sustained 429s over a window (e.g., more than 50 in 10 minutes). Most products pair this with an in-product banner; the email catches users whose dashboards aren't open. Include:
- The key name and last-4 of the prefix
- Current rate limit and the rate they're hitting
- A link to the current usage graph
- One sentence on how to request a higher limit
Billing threshold alerts
AI products that allow overage billing should let users set spend caps at the workspace level. The threshold email fires when the workspace crosses a configured number ($100, $500, $1000), not when the period closes. Users who set caps want to know beforethe billing period ends, while there's still time to throttle their own integration.
import { resend } from "@/lib/resend";
import BillingThreshold from "@/emails/billing-threshold";
type ThresholdEvent = {
workspaceId: string;
ownerEmail: string;
ownerName: string;
currency: "USD" | "EUR" | "GBP";
threshold: number;
spent: number;
cap: number | null;
periodEndsAt: string;
};
export async function dispatchBillingThreshold(event: ThresholdEvent) {
const recentlySent = await alreadySentThisWindow(
event.workspaceId,
event.threshold,
);
if (recentlySent) return;
await resend.emails.send({
from: "billing@yourdomain.com",
to: event.ownerEmail,
subject: `Spend alert: ${formatMoney(event.spent, event.currency)} this period`,
react: BillingThreshold({
ownerName: event.ownerName,
currency: event.currency,
spent: event.spent,
threshold: event.threshold,
cap: event.cap,
periodEndsAt: event.periodEndsAt,
}),
headers: {
"X-Entity-Ref-ID": `billing-threshold-${event.workspaceId}-${event.threshold}`,
},
});
await markSentThisWindow(event.workspaceId, event.threshold);
}Two production details in that snippet: the deduplication check before sending (a workspace can flap across a threshold multiple times), and the X-Entity-Ref-ID header, which makes the email idempotent at the provider level. Resend uses this header to deduplicate retries from your own infra.
Production checklist before you launch
Before turning on the email pipeline for an AI product, run through this list. Each item below has bitten teams in production.
- Cooldowns on every threshold email. Burst usage can trip 70% and 90% warnings within minutes. Suppress duplicates within 24h.
- Per-key idempotency headers. Set
X-Entity-Ref-IDon every send so your own retry logic doesn't double-deliver. - Notification preferences in settings.Users on heavy plans want to mute receipts. Required for usage-based products if you don't want to land in spam.
- Separate streams for transactional and digests.Use separate Resend audiences or Postmark message streams so a high unsubscribe rate on digests doesn't poison transactional deliverability.
- Receipts include the dollar value, not just credits. Credits are abstract. Dollars are concrete and make the spend decision obvious.
- Security emails bypass digests. Always send immediately for API key creation, revocation, and rotation. Never batch.
- Cycle reset emails reference last cycle. “Last month you ran 142 jobs and used 84% of your credits.” That's the hook to bring a user back at the start of the new cycle.
- Templates render in dark mode. AI users live in dark-mode email clients. Test with the dark mode email guide before shipping.
Build the templates yourself, or use a pack?
For most teams shipping an AI product, the email layer is not where the product wins or loses. The model, the UX, the latency - those are the differentiation. Email exists to keep users informed and prevent billing surprises. The actual choice is whether to spend two sprints designing and testing eight templates across email clients, or drop in something already built and customize the brand bits.
- Eight to ten production-ready templates ship in a day, not a sprint
- Already tested in Outlook, Gmail dark mode, Apple Mail, mobile
- Typed props and discriminated unions so the template can't drift from your event types
- Brand customization is a tokens-and-colors edit, not a rewrite
- Total control over every layout decision
- No external dependency on a template library version
- Easier to defend in code review if your team has strong email design culture
- Realistically 2-3 weeks of design, build, and cross-client testing
Related reading
If you're wiring this into a Next.js app, these adjacent guides cover the infrastructure pieces:
- Email Queue Patterns for Next.js - the BullMQ and Vercel Cron patterns that make threshold emails reliable.
- Stripe Webhook Emails in Next.js - the webhook plumbing that fires billing threshold and overage alerts.
- Server Actions + React Email - the dispatcher pattern that keeps email-sending out of API routes.