Free email template
New sign-in alert email template
A security alert for a sign-in from a new device, with details and a "secure my account" path. 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 new sign-in alert email needs.
The template covers the email side of all six. Points 1 and 5 also depend on your app.
Only for new devices
Send it when a device or browser signs in for the first time, not on every sign-in. An alert people see daily is an alert they stop reading.
Details people can recognise
Time with a time zone, device and browser, a rough location and the IP address. Enough for someone to say "that was me" or "that wasn't".
Say the location is approximate
Locations from an IP address can be a city or more off. The template says so, which saves a lot of worried support emails.
Nothing to do if it was them
Most alerts are the account owner on a new laptop. Say plainly that there's nothing to do, so people don't panic.
One path for "this wasn't me"
The template's button promises to help change the password and sign out every other session. Make sure the page behind it does both.
Nothing to type into the email
Never ask for a password or a code in a security alert. Phishers copy these emails, and people should learn that the real one only ever links to your site.
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, P, Small, Title } from '../../components'
import type { TemplateMeta } from '../../components/meta'
import { plainText, textDetails } from '../../components/text'
import { theme } from '../../theme'
export type NewSignInProps = {
name: string
signedInAt: string
device: string
location: string
ipAddress: string
secureUrl: string
}
export const meta: TemplateMeta = {
name: 'New sign-in alert',
description: 'A security alert for a sign-in from a new device, with details and a "secure my account" path.',
group: 'Account',
subject: `New sign-in to your ${theme.brand.name} account`,
}
export default function NewSignIn(props: NewSignInProps) {
const { name, signedInAt, device, location, ipAddress, secureUrl } = props
return (
<Layout
preview={`Your account was signed in to from ${device} in ${location}.`}
footerNote="This is a security email about your account, so you get it even if other emails are turned off."
>
<Title>New sign-in to your account</Title>
<P>Hi {name}, your account was just signed in to from a device we haven't seen before.</P>
<Details
rows={[
['When', signedInAt],
['Device', device],
['Location', location],
['IP address', ipAddress],
]}
/>
<P>
<strong>If this was you</strong>, there's nothing to do.
</P>
<P>
<strong>If it wasn't</strong>, secure your account now. We'll help you change your password and sign out
every other session.
</P>
<Button href={secureUrl} tone="danger">
Secure my account
</Button>
<Small style={{ margin: 0 }}>Locations are estimated from the IP address and may not be exact.</Small>
</Layout>
)
}
NewSignIn.PreviewProps = {
name: 'Ada',
signedInAt: 'October 16, 2026 at 9:41 AM UTC',
device: 'Chrome on macOS',
location: 'Lisbon, Portugal',
ipAddress: '203.0.113.42',
secureUrl: 'https://example.com/account/security',
} satisfies NewSignInProps
export function text(props: NewSignInProps) {
const { name, signedInAt, device, location, ipAddress, secureUrl } = props
return plainText(
'New sign-in to your account',
`Hi ${name}, your account was just signed in to from a device we haven't seen before.`,
textDetails([
['When', signedInAt],
['Device', device],
['Location', location],
['IP address', ipAddress],
]),
"If this was you, there's nothing to do.",
`If it wasn't, secure your account now. We'll help you change your password and sign out every other session:\n${secureUrl}`,
'Locations are estimated from the IP address and may not be exact.',
)
}
<mjml lang="en" dir="ltr">
<mj-head>
<mj-title>New sign-in to your account</mj-title>
<mj-preview>Your account was signed in to from {{device}} in {{location}}.</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">New sign-in to your account</mj-text>
<mj-text>Hi {{name}}, your account was just signed in to from a device we haven't seen before.</mj-text>
<mj-table>
<tr><td class="em-label em-muted">When</td><td class="em-value em-text">{{signed_in_at}}</td></tr>
<tr><td class="em-label em-muted em-row-border em-border">Device</td><td class="em-value em-text em-row-border em-border">{{device}}</td></tr>
<tr><td class="em-label em-muted em-row-border em-border">Location</td><td class="em-value em-text em-row-border em-border">{{location}}</td></tr>
<tr><td class="em-label em-muted em-row-border em-border">IP address</td><td class="em-value em-text em-row-border em-border">{{ip_address}}</td></tr>
</mj-table>
<mj-text><strong>If this was you</strong>, there's nothing to do.</mj-text>
<mj-text><strong>If it wasn't</strong>, secure your account now. We'll help you change your password and sign out every other session.</mj-text>
<mj-button mj-class="danger" href="{{secure_url}}">Secure my account</mj-button>
<mj-text mj-class="small" padding="0">Locations are estimated from the IP address and may not be exact.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 8px 0">
<mj-column>
<mj-text mj-class="footer">This is a security email about your account, so you get it even if other emails are turned off.</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>
Your account was signed in to from {{device}} in {{location}}.
</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">
Your account was signed in to from {{device}} in {{location}}.
<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">
New sign-in to your account
</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}}, your account was just signed
in to from a device we haven't seen
before.
</p>
<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%">
When
</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">
{{signed_in_at}}
</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%">
Device
</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">
{{device}}
</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%">
Location
</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">
{{location}}
</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%">
IP address
</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">
{{ip_address}}
</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">
<strong>If this was you</strong>,
there's nothing to do.
</p>
<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">
<strong>If it wasn't</strong>, secure
your account now. We'll help you
change your password and sign out every
other session.
</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-danger"
href="{{secure_url}}"
style="line-height:20px;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#b42318;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"
>Secure my account</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">
Locations are estimated from the IP
address and may not be exact.
</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">
This is a security email about your account, so
you get it even if other emails are turned off.
</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>
New sign-in to your account
Hi {{name}}, your account was just signed in to from a device we haven't seen before.
When: {{signed_in_at}}
Device: {{device}}
Location: {{location}}
IP address: {{ip_address}}
If this was you, there's nothing to do.
If it wasn't, secure your account now. We'll help you change your password and sign out every other session:
{{secure_url}}
Locations are estimated from the IP address and may not be exact.
--
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/new-sign-in/new-sign-in.html \
--alias new-sign-in \
--name "New sign-in alert" \
--subject "New sign-in to your Acme account" \
--text templates/new-sign-in/new-sign-in.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: 'new-sign-in',
variables: {
name: 'Ada',
signed_in_at: 'October 16, 2026 at 9:41 AM UTC',
device: 'Chrome on macOS',
location: 'Lisbon, Portugal',
ip_address: '203.0.113.42',
secure_url: 'https://example.com/account/security',
},
},
})new-sign-in.tsx, 79 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/new-sign-in/new-sign-in.html \
--alias new-sign-in \
--name "New sign-in alert" \
--subject "New sign-in to your Acme account" \
--text templates/new-sign-in/new-sign-in.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: 'new-sign-in',
variables: {
name: 'Ada',
signed_in_at: 'October 16, 2026 at 9:41 AM UTC',
device: 'Chrome on macOS',
location: 'Lisbon, Portugal',
ip_address: '203.0.113.42',
secure_url: 'https://example.com/account/security',
},
},
})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
Security alerts get copied by phishers, so keep the subject factual and consistent. Name the product and say what happened.
- New sign-in to your Acme accountDefaultFactual and specific to your product.
- New sign-in to Acme from {{device}}GoodLets people dismiss it from the notification if they recognise the device.
- Acme security alert: new sign-inGoodEasy to filter and search for as a security email.
- Your account has been compromisedAvoidIt claims more than you know, frightens people who signed in themselves, and matches phishing.
A security alert only helps if it arrives while it matters
If the sign-in wasn't the account owner, the minutes after it count. And because these emails are copied by phishers, people need to be able to tell the real one apart.
SPF, DKIM and DMARC for transactional email →- Publish DMARC with a quarantine or reject policy, so nobody else can send alerts as your domain.
- Keep it on a transactional stream, apart from newsletters and marketing.
- Send from the same address as your other account emails, every time.
- Groundmail sends this template by alias from your own domain, on our own mail servers. Send it with Groundmail
More account templates
All email templates →- Welcome email templateA short first email after sign-up: three next steps and one button.
- Team invite email templateInvites someone to a team or workspace, naming who sent it and the role they get.
- Account deletion email templateConfirms a deletion request and offers a grace period to restore the account.
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.