Free email template
Usage alert email template
Warns before a plan limit is hit, with a progress bar and the reset date. Free and MIT-licensed, in React Email, MJML, HTML and plain text, with light and dark mode built in.
- MIT license
- Works with any email provider
- Light and dark mode
What every usage alert email needs.
The template covers five. Point 5 depends on your plan rules, so add a line saying what happens at the limit.
Before the limit, not at it
Warn at a threshold like 80%, while there's still time to act. Send each alert once per billing period, not on every request after it.
The real numbers
Used and limit, with the metric named the way your pricing page names it. "80,000 of 100,000 monthly API requests" beats "you're nearly out".
A bar people read at a glance
The template's progress bar shows how close they are before they read a word.
When it resets
If the reset is two days away, waiting may be the right answer. Give people the date so they can decide.
What happens at 100%
Does the service stop, slow down or bill for extra use? Say which. People plan differently for each.
A way to change these alerts
Heavy users hit thresholds every month. Let them pick their own, or turn them off, from account settings.
The code. Copy the format your stack uses.
Change the brand name, logo and colors once in theme.ts and every template follows.
import * as React from 'react'
import { Button, Details, Layout, Meter, P, Small, Title } from '../../components'
import type { TemplateMeta } from '../../components/meta'
import { plainText, textDetails } from '../../components/text'
export type UsageAlertProps = {
name: string
metric: string
usagePercent: string
used: string
limit: string
resetsOn: string
upgradeUrl: string
}
export const meta: TemplateMeta = {
name: 'Usage alert',
description: 'Warns before a plan limit is hit, with a progress bar and the reset date.',
group: 'Billing',
subject: "You've used {{usage_percent}}% of your {{metric}}",
}
export default function UsageAlert(props: UsageAlertProps) {
const { name, metric, usagePercent, used, limit, resetsOn, upgradeUrl } = props
return (
<Layout
preview={`You've used ${used} of ${limit} ${metric}. Your limit resets on ${resetsOn}.`}
footerNote="You're receiving this because usage alerts are on for your account."
>
<Title>
You've used {usagePercent}% of your {metric}
</Title>
<P>Hi {name}, you're getting close to your plan's limit for this billing period.</P>
<Meter percent={usagePercent} />
<Details
rows={[
['Used', `${used} of ${limit}`],
['Resets on', resetsOn],
]}
/>
<P>To avoid interruptions, upgrade your plan before you reach the limit.</P>
<Button href={upgradeUrl}>Upgrade plan</Button>
<Small style={{ margin: 0 }}>You can change when you get these alerts in your account settings.</Small>
</Layout>
)
}
UsageAlert.PreviewProps = {
name: 'Ada',
metric: 'monthly API requests',
usagePercent: '80',
used: '80,000',
limit: '100,000',
resetsOn: 'November 1, 2026',
upgradeUrl: 'https://example.com/billing/plans',
} satisfies UsageAlertProps
export function text(props: UsageAlertProps) {
const { name, metric, usagePercent, used, limit, resetsOn, upgradeUrl } = props
return plainText(
`You've used ${usagePercent}% of your ${metric}`,
`Hi ${name}, you're getting close to your plan's limit for this billing period.`,
textDetails([
['Used', `${used} of ${limit}`],
['Resets on', resetsOn],
]),
`To avoid interruptions, upgrade your plan before you reach the limit: ${upgradeUrl}`,
'You can change when you get these alerts in your account settings.',
)
}
<mjml lang="en" dir="ltr">
<mj-head>
<mj-title>You've used {{usage_percent}}% of your {{metric}}</mj-title>
<mj-preview>You've used {{used}} of {{limit}} {{metric}}. Your limit resets on {{resets_on}}.</mj-preview>
<!-- theme:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-raw>
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
</mj-raw>
<mj-attributes>
<mj-all font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" />
<mj-body background-color="#f4f4f5" width="560px" />
<mj-wrapper padding="40px 16px" />
<mj-section padding="0" />
<mj-column padding="0" />
<mj-text padding="0 0 16px" font-size="15px" line-height="24px" color="#18181b" css-class="em-text" />
<mj-button align="left" background-color="#2f5bd3" color="#ffffff" font-size="15px" font-weight="600" line-height="20px" border-radius="8px" inner-padding="12px 22px" padding="8px 0 24px" css-class="em-button" />
<mj-divider border-width="1px" border-color="#e4e4e7" padding="12px 0 28px" css-class="em-rule" />
<mj-table padding="0 0 24px" font-size="14px" line-height="20px" color="#18181b" />
<mj-class name="card" background-color="#ffffff" border="1px solid #e4e4e7" border-radius="12px" padding="40px" css-class="em-card" />
<mj-class name="h1" font-size="24px" line-height="32px" font-weight="650" letter-spacing="-0.3px" css-class="em-text em-h1" />
<mj-class name="small" font-size="13px" line-height="20px" color="#52525b" padding="0 0 12px" css-class="em-muted" />
<mj-class name="danger" background-color="#b42318" css-class="em-button-danger" />
<mj-class name="footer" font-size="12px" line-height="18px" color="#52525b" align="center" padding="0 0 6px" css-class="em-muted" />
</mj-attributes>
<mj-style inline="inline">
.em-label { padding: 10px 0; color: #52525b; width: 45%; vertical-align: top; }
.em-value { padding: 10px 0; color: #18181b; text-align: right; vertical-align: top; }
.em-row-border { border-top: 1px solid #e4e4e7; }
.em-total { padding: 12px 0 10px; color: #18181b; font-weight: 650; border-top: 2px solid #18181b; vertical-align: top; }
.em-total-value { padding: 12px 0 10px; color: #18181b; font-weight: 650; border-top: 2px solid #18181b; text-align: right; vertical-align: top; }
.em-codebox { margin: 8px 0 8px; padding: 18px 12px; background-color: #f4f4f5; border: 1px solid #e4e4e7; border-radius: 8px; text-align: center; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace; font-size: 32px; line-height: 40px; font-weight: 700; letter-spacing: 8px; color: #18181b; }
.em-callout { padding: 14px 16px; background-color: #f4f4f5; border: 1px solid #e4e4e7; border-radius: 8px; font-size: 14px; line-height: 22px; color: #18181b; }
.em-track { height: 8px; font-size: 1px; line-height: 8px; border-radius: 999px; background-color: #f4f4f5; }
.em-fill { height: 8px; font-size: 1px; line-height: 8px; border-radius: 999px; background-color: #2f5bd3; }
.em-link { color: #2f5bd3; text-decoration: underline; }
.em-step { color: #52525b; }
</mj-style>
<mj-style>
body { -webkit-font-smoothing: antialiased; }
:root { color-scheme: light dark; supported-color-schemes: light dark; }
.em-logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
body, .em-page { background-color: #0e0e10 !important; }
.em-card, .em-card > table, .em-card > table > tbody > tr > td { background-color: #18181b !important; border-color: #2e2e33 !important; }
.em-text, .em-text > div { color: #f4f4f5 !important; }
.em-muted, .em-muted > div, .em-muted a { color: #a1a1aa !important; }
.em-link, .em-text a { color: #7a9bff !important; }
.em-wordmark, .em-wordmark > div, .em-wordmark a { color: #f4f4f5 !important; }
.em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
.em-border { border-color: #2e2e33 !important; }
.em-strong-border { border-color: #f4f4f5 !important; }
.em-rule, .em-rule p { border-top-color: #2e2e33 !important; }
.em-track { background-color: #232327 !important; }
.em-fill { background-color: #7a9bff !important; }
a.em-button, .em-button a, .em-button td[bgcolor] { background-color: #7a9bff !important; color: #0b0d14 !important; border-color: #7a9bff !important; }
a.em-button-danger, .em-button-danger a, .em-button-danger td[bgcolor] { background-color: #f97066 !important; color: #0b0d14 !important; border-color: #f97066 !important; }
.em-logo-light { display: none !important; }
.em-logo-dark { display: block !important; }
}
[data-ogsc] .em-card, [data-ogsc] .em-card > table, [data-ogsc] .em-card > table > tbody > tr > td { border-color: #2e2e33 !important; }
[data-ogsc] .em-text, [data-ogsc] .em-text > div { color: #f4f4f5 !important; }
[data-ogsc] .em-muted, [data-ogsc] .em-muted > div, [data-ogsc] .em-muted a { color: #a1a1aa !important; }
[data-ogsc] .em-link, [data-ogsc] .em-text a { color: #7a9bff !important; }
[data-ogsc] .em-wordmark, [data-ogsc] .em-wordmark > div, [data-ogsc] .em-wordmark a { color: #f4f4f5 !important; }
[data-ogsc] .em-subtle { border-color: #2e2e33 !important; }
[data-ogsc] .em-border { border-color: #2e2e33 !important; }
[data-ogsc] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsc] .em-rule, [data-ogsc] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsc] a.em-button, [data-ogsc] .em-button a, [data-ogsc] .em-button td[bgcolor] { color: #0b0d14 !important; border-color: #7a9bff !important; }
[data-ogsc] a.em-button-danger, [data-ogsc] .em-button-danger a, [data-ogsc] .em-button-danger td[bgcolor] { color: #0b0d14 !important; border-color: #f97066 !important; }
[data-ogsb] body, [data-ogsb] .em-page { background-color: #0e0e10 !important; }
[data-ogsb] .em-card, [data-ogsb] .em-card > table, [data-ogsb] .em-card > table > tbody > tr > td { background-color: #18181b !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-border { border-color: #2e2e33 !important; }
[data-ogsb] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsb] .em-rule, [data-ogsb] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsb] .em-track { background-color: #232327 !important; }
[data-ogsb] .em-fill { background-color: #7a9bff !important; }
[data-ogsb] a.em-button, [data-ogsb] .em-button a, [data-ogsb] .em-button td[bgcolor] { background-color: #7a9bff !important; border-color: #7a9bff !important; }
[data-ogsb] a.em-button-danger, [data-ogsb] .em-button-danger a, [data-ogsb] .em-button-danger td[bgcolor] { background-color: #f97066 !important; border-color: #f97066 !important; }
@media only screen and (max-width: 600px) {
.em-card > table > tbody > tr > td { padding: 32px 24px !important; }
.em-h1, .em-h1 > div { font-size: 22px !important; line-height: 30px !important; }
}
</mj-style>
<!-- theme:end -->
</mj-head>
<mj-body>
<!-- header:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-wrapper css-class="em-page">
<mj-section padding="0 8px 24px">
<mj-column>
<mj-text padding="0" font-size="18px" line-height="28px" font-weight="700" letter-spacing="-0.2px" css-class="em-wordmark"><a href="https://example.com" style="color:#18181b;text-decoration:none;">Acme</a></mj-text>
</mj-column>
</mj-section>
<!-- header:end -->
<mj-section mj-class="card">
<mj-column>
<mj-text mj-class="h1">You've used {{usage_percent}}% of your {{metric}}</mj-text>
<mj-text>Hi {{name}}, you're getting close to your plan's limit for this billing period.</mj-text>
<mj-text padding="4px 0 24px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td class="em-track"><table role="presentation" width="{{usage_percent}}%" style="width:{{usage_percent}}%;" cellpadding="0" cellspacing="0" border="0"><tr><td class="em-fill"> </td></tr></table></td></tr></table></mj-text>
<mj-table>
<tr><td class="em-label em-muted">Used</td><td class="em-value em-text">{{used}} of {{limit}}</td></tr>
<tr><td class="em-label em-muted em-row-border em-border">Resets on</td><td class="em-value em-text em-row-border em-border">{{resets_on}}</td></tr>
</mj-table>
<mj-text>To avoid interruptions, upgrade your plan before you reach the limit.</mj-text>
<mj-button href="{{upgrade_url}}">Upgrade plan</mj-button>
<mj-text mj-class="small" padding="0">You can change when you get these alerts in your account settings.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 8px 0">
<mj-column>
<mj-text mj-class="footer">You're receiving this because usage alerts are on for your account.</mj-text>
</mj-column>
</mj-section>
<!-- footer:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-section padding="0 8px">
<mj-column>
<mj-text mj-class="footer">Acme Inc. · 100 Market Street · San Francisco, CA 94105</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<!-- footer:end -->
</mj-body>
</mjml>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title>
You've used {{used}} of {{limit}} {{metric}}. Your limit resets on
{{resets_on}}.
</title>
<style>
:root { color-scheme: light dark; supported-color-schemes: light dark; }
.em-logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
.em-page, .em-page > table > tbody > tr > td { background-color: #0e0e10 !important; }
.em-card { background-color: #18181b !important; border-color: #2e2e33 !important; }
.em-text, .em-text > div { color: #f4f4f5 !important; }
.em-muted, .em-muted > div, .em-muted a { color: #a1a1aa !important; }
.em-link, .em-text a { color: #7a9bff !important; }
.em-wordmark, .em-wordmark > div, .em-wordmark a { color: #f4f4f5 !important; }
.em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
.em-border { border-color: #2e2e33 !important; }
.em-strong-border { border-color: #f4f4f5 !important; }
.em-rule, .em-rule p { border-top-color: #2e2e33 !important; }
.em-track { background-color: #232327 !important; }
.em-fill { background-color: #7a9bff !important; }
a.em-button, .em-button a, .em-button td[bgcolor] { background-color: #7a9bff !important; color: #0b0d14 !important; border-color: #7a9bff !important; }
a.em-button-danger, .em-button-danger a, .em-button-danger td[bgcolor] { background-color: #f97066 !important; color: #0b0d14 !important; border-color: #f97066 !important; }
.em-logo-light { display: none !important; }
.em-logo-dark { display: block !important; }
}
[data-ogsc] .em-card { border-color: #2e2e33 !important; }
[data-ogsc] .em-text, [data-ogsc] .em-text > div { color: #f4f4f5 !important; }
[data-ogsc] .em-muted, [data-ogsc] .em-muted > div, [data-ogsc] .em-muted a { color: #a1a1aa !important; }
[data-ogsc] .em-link, [data-ogsc] .em-text a { color: #7a9bff !important; }
[data-ogsc] .em-wordmark, [data-ogsc] .em-wordmark > div, [data-ogsc] .em-wordmark a { color: #f4f4f5 !important; }
[data-ogsc] .em-subtle { border-color: #2e2e33 !important; }
[data-ogsc] .em-border { border-color: #2e2e33 !important; }
[data-ogsc] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsc] .em-rule, [data-ogsc] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsc] a.em-button, [data-ogsc] .em-button a, [data-ogsc] .em-button td[bgcolor] { color: #0b0d14 !important; border-color: #7a9bff !important; }
[data-ogsc] a.em-button-danger, [data-ogsc] .em-button-danger a, [data-ogsc] .em-button-danger td[bgcolor] { color: #0b0d14 !important; border-color: #f97066 !important; }
[data-ogsb] .em-page, [data-ogsb] .em-page > table > tbody > tr > td { background-color: #0e0e10 !important; }
[data-ogsb] .em-card { background-color: #18181b !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-border { border-color: #2e2e33 !important; }
[data-ogsb] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsb] .em-rule, [data-ogsb] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsb] .em-track { background-color: #232327 !important; }
[data-ogsb] .em-fill { background-color: #7a9bff !important; }
[data-ogsb] a.em-button, [data-ogsb] .em-button a, [data-ogsb] .em-button td[bgcolor] { background-color: #7a9bff !important; border-color: #7a9bff !important; }
[data-ogsb] a.em-button-danger, [data-ogsb] .em-button-danger a, [data-ogsb] .em-button-danger td[bgcolor] { background-color: #f97066 !important; border-color: #f97066 !important; }
@media only screen and (max-width: 600px) {
.em-card-body > tbody > tr > td { padding: 32px 24px !important; }
.em-h1, .em-h1 > div { font-size: 22px !important; line-height: 30px !important; }
}
</style>
</head>
<body
class="em-page"
dir="ltr"
lang="en"
style="background-color:#f4f4f5;margin:0;padding:0">
<div
style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0"
data-skip-in-text="true">
You've used {{used}} of {{limit}} {{metric}}. Your limit resets on
{{resets_on}}.
<div>
</div>
</div>
<table
border="0"
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
align="center">
<tbody>
<tr>
<td
dir="ltr"
lang="en"
style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;-webkit-font-smoothing:antialiased">
<table
align="center"
width="100%"
class="em-page"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="max-width:560px;width:100%;margin:0 auto">
<tbody>
<tr style="width:100%">
<td style="padding:40px 16px">
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation">
<tbody>
<tr>
<td style="padding:0 8px 24px">
<a
href="https://example.com"
style="color:#067df7;text-decoration-line:none;text-decoration:none"
target="_blank"
><p
class="em-wordmark"
style="font-size:18px;line-height:28px;margin:0;font-weight:700;letter-spacing:-0.2px;color:#18181b;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">
Acme
</p></a
>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-card"
style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px">
<tbody>
<tr>
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-card-body">
<tbody>
<tr>
<td style="padding:40px 40px">
<h1
class="em-text em-h1"
style="margin:0 0 16px;font-size:24px;line-height:32px;font-weight:650;letter-spacing:-0.3px;color:#18181b">
You've used {{usage_percent}}% of
your {{metric}}
</h1>
<p
class="em-text"
style="font-size:15px;line-height:24px;margin:0 0 16px;color:#18181b;margin-top:0;margin-right:0;margin-bottom:16px;margin-left:0">
Hi {{name}}, you're getting close to
your plan's limit for this billing
period.
</p>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
style="margin:4px 0 24px">
<tbody>
<tr>
<td
class="em-track"
style="height:8px;font-size:1px;line-height:8px;border-radius:999px;background-color:#f4f4f5">
<table
role="presentation"
width="{{usage_percent}}%"
cellpadding="0"
cellspacing="0"
border="0"
style="width:{{usage_percent}}%">
<tbody>
<tr>
<td
class="em-fill"
style="height:8px;font-size:1px;line-height:8px;border-radius:999px;background-color:#2f5bd3">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin:0 0 24px">
<tbody>
<tr>
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-border">
<tbody style="width:100%">
<tr style="width:100%">
<td
class="em-muted"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#52525b;width:45%">
Used
</td>
<td
class="em-text"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#18181b;text-align:right">
{{used}} of {{limit}}
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-border"
style="border-top:1px solid #e4e4e7">
<tbody style="width:100%">
<tr style="width:100%">
<td
class="em-muted"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#52525b;width:45%">
Resets on
</td>
<td
class="em-text"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#18181b;text-align:right">
{{resets_on}}
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p
class="em-text"
style="font-size:15px;line-height:24px;margin:0 0 16px;color:#18181b;margin-top:0;margin-right:0;margin-bottom:16px;margin-left:0">
To avoid interruptions, upgrade your plan
before you reach the limit.
</p>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin:8px 0 24px">
<tbody>
<tr>
<td>
<a
class="em-button"
href="{{upgrade_url}}"
style="line-height:20px;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#2f5bd3;color:#ffffff;font-size:15px;font-weight:600;border-radius:8px;padding:12px 22px;padding-top:12px;padding-right:22px;padding-bottom:12px;padding-left:22px"
target="_blank"
><span
><!--[if mso]><i style="mso-font-width:366.66666666666663%;mso-text-raise:18px" hidden>   </i><![endif]--></span
><span
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px"
>Upgrade plan</span
><span
><!--[if mso]><i style="mso-font-width:366.66666666666663%" hidden>   ​</i><![endif]--></span
></a
>
</td>
</tr>
</tbody>
</table>
<p
class="em-muted"
style="font-size:13px;line-height:20px;margin:0;color:#52525b;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">
You can change when you get these alerts
in your account settings.
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="text-align:center">
<tbody>
<tr>
<td style="padding:24px 8px 0">
<p
class="em-muted"
style="font-size:12px;line-height:18px;margin:0 0 6px;color:#52525b;margin-top:0;margin-right:0;margin-bottom:6px;margin-left:0">
You're receiving this because usage alerts
are on for your account.
</p>
<p
class="em-muted"
style="font-size:12px;line-height:18px;margin:0 0 6px;color:#52525b;margin-top:0;margin-right:0;margin-bottom:6px;margin-left:0">
Acme Inc. · 100 Market Street · San Francisco, CA
94105
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
You've used {{usage_percent}}% of your {{metric}}
Hi {{name}}, you're getting close to your plan's limit for this billing period.
Used: {{used}} of {{limit}}
Resets on: {{resets_on}}
To avoid interruptions, upgrade your plan before you reach the limit: {{upgrade_url}}
You can change when you get these alerts in your account settings.
--
Acme
Acme Inc. · 100 Market Street · San Francisco, CA 94105
# Create the template in your account once
npx -y --package=groundmail-cli -- groundmail templates push templates/usage-alert/usage-alert.html \
--alias usage-alert \
--name "Usage alert" \
--subject "You've used {{usage_percent}}% of your {{metric}}" \
--text templates/usage-alert/usage-alert.txt \
--publish
// Then send it by alias from your app
import { Groundmail } from 'groundmail'
const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
await groundmail.emails.send({
from: 'Acme <noreply@yourdomain.com>',
to: 'ada@example.com',
template: {
id: 'usage-alert',
variables: {
name: 'Ada',
metric: 'monthly API requests',
usage_percent: '80',
used: '80,000',
limit: '100,000',
resets_on: 'November 1, 2026',
upgrade_url: 'https://example.com/billing/plans',
},
},
})usage-alert.tsx, 70 lines, shown in full (scroll inside the panel). Typed props. Copy it with theme.ts and components/ from the repo. View on GitHub
Send it with Groundmail
Create the template in your account once, then send it by alias from your app.
npx -y --package=groundmail-cli -- groundmail templates push templates/usage-alert/usage-alert.html \
--alias usage-alert \
--name "Usage alert" \
--subject "You've used {{usage_percent}}% of your {{metric}}" \
--text templates/usage-alert/usage-alert.txt \
--publishimport { Groundmail } from 'groundmail'
const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
await groundmail.emails.send({
from: 'Acme <noreply@yourdomain.com>',
to: 'ada@example.com',
template: {
id: 'usage-alert',
variables: {
name: 'Ada',
metric: 'monthly API requests',
usage_percent: '80',
used: '80,000',
limit: '100,000',
resets_on: 'November 1, 2026',
upgrade_url: 'https://example.com/billing/plans',
},
},
})The CLI command and the Node.js send from the template's README, which also has Python, PHP and cURL. View on GitHub
Subject lines
The number is the news. Put it first and name the limit it's about.
- You've used {{usage_percent}}% of your {{metric}}DefaultThe number and the metric, readable from the notification.
- Acme: {{usage_percent}}% of your {{metric}} usedGoodBrand first, for people who use several metered services.
- You're close to your Acme {{metric}} limitGoodFor a second alert, when the exact figure matters less than the warning.
- Upgrade now and save 20%!AvoidA usage alert is a service message. Turning it into a sales pitch makes it marketing, to people and to filters.
An alert after the limit is an outage report
Usage alerts are time-sensitive. One that's delayed or filtered arrives after the service has already stopped, which is the moment it was meant to prevent.
Google, Yahoo and Microsoft sender rules →- Keep it on a transactional stream, apart from newsletters and marketing.
- Send from your own domain with SPF, DKIM and DMARC set up.
- Send once per threshold per period, so a busy account doesn't get the same alert a hundred times.
- Groundmail sends this template by alias from your own domain, on our own mail servers. Send it with Groundmail
More billing templates
All email templates →- Receipt email templateA payment receipt with line items, tax, total and a link to the full invoice.
- Payment failed email templateDunning email for a declined card: what failed, when it retries, and one fix button.
- Subscription renewal email templateAn upcoming-renewal reminder with the amount, date and card, sent before the charge.
Questions
Is it free to use?
Yes. The templates are MIT-licensed: use them in commercial products and change them however you like.
Do I need Groundmail to use it?
No. The HTML and text files use plain placeholders that work with most providers' template engines and with Handlebars or Mustache. Sending through Groundmail just saves a step.
Does it work in dark mode and Outlook?
Yes. The full HTML file carries dark-mode styles for Apple Mail and Outlook.com, and the extra markup desktop Outlook needs to draw the button.
How do I change the colors and logo?
Edit theme.ts once (name, logo, address and colors for light and dark) and rebuild. Every template in the set picks it up.