Templates13 min read

AI SaaS Email Patterns: Usage Receipts, Credit Alerts, and Quota Warnings

AI products charge by usage, not by seat. Build the email patterns - generation receipts, 70/90/100% credit warnings, API key alerts, and billing thresholds - that fit usage-based billing in Next.js with React Email.

R

React Emails Pro

April 30, 2026

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.

TL;DR
  • 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.

EmailTriggerIntentFrequency
Waitlist confirmedUser joins waitlistConfirm spot, set expectationOnce per signup
Access grantedBeta or invite-only access opensConvert waitlist to active userOnce per user
Generation receiptHigh-cost run completes (fine-tune, video, agent)Confirm spend, prove valuePer high-cost action
Usage warning70% / 90% of credits usedPre-empt block, prompt upgradeTwice per cycle, max
Limit reached100% of credits usedExplain block, give 1-click upgradeOnce per cycle
Quota resetNew billing cycle startsRe-engage, surface what they did last cycleOnce per cycle
API key createdNew key generatedConfirm legitimate creation, anti-fraudPer event
Model or feature updateNew model rolled out, plan unlocks featureDrive re-engagement, surface valuePer release
Rate limit warningSustained 429s on an API keyHelp debug, prevent runaway costsThrottled to 1/hr
Billing thresholdSpend hits configurable cap (e.g., $500)Prevent overage shockPer 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.
Cursor restructured pricing four times in under two years. ChatGPT did similar. Email is the only durable channel where you can explain a change in plan, model, or limit before users open the app and discover it themselves.

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.

emails/generation-receipt.tsx
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 &rarr; 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.

For products that bill in tokens (LLM apps), include both the raw token count and the cost in dollars. Token counts feel abstract; dollars anchor users to the value they're getting.

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:

ThresholdToneCTA
70%Informational, neutralView usage / Upgrade plan
90%Concrete, action-orientedUpgrade plan / Add credits
100%Explanatory, helpfulUpgrade now (one click)
Cycle resetRe-engagementPick up where you left off

A usage warning template

emails/usage-warning.tsx
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.

Don't fire the 90% warning if the 70% warning was sent less than 24 hours ago. Burst usage trips both thresholds in minutes and the second email reads as panicked. Add a cooldown.

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.
emails/api-key-created.tsx
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&apos;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>
  );
}
Include the IP address and user-agent on every security email. Users catch suspicious activity faster than your fraud heuristics, and these details cost nothing to capture from the request that created the key.

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:

  1. One sentence on what's new (model name, capability).
  2. A concrete before/after metric (latency dropped 30%, output quality score up 12 points).
  3. 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
Throttle the rate-limit email itself. Sending one per 429 spike is the fastest way to land in spam. One email per key per hour, max, with details aggregated for that window.

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.

lib/billing-threshold-dispatcher.ts
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.

Use a pre-built AI SaaS pack
  • 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
Build from scratch
  • 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:


Key takeaway
AI SaaS products need a different email taxonomy than seat-based SaaS. At a minimum, you need usage warnings at 70/90/100%, generation receipts with cost in dollars (not just credits), per-key API security emails, and billing threshold alerts that fire mid-cycle. Get those seven or eight templates right, add a 24h cooldown on threshold sends, and a separate stream for digests, and the email layer stops being a churn risk.
R

React Emails Pro

Team

Building production-ready email templates with React Email. Writing about transactional email best practices, deliverability, and developer tooling.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates