Team invite email template: what to include and how to send it
A team invite email template you can copy, what every SaaS invitation needs, how long links should last, and how to send one with an API call.
The LuneaMail team · 24 October 2026 · 9 min read
A team invite email tells someone who invited them, to which workspace, with which role, and gives them one button to accept. A good subject line is "{{inviterName}} invited you to {{workspaceName}} on {{product}}". Send it from your server the moment the invite is created, from your own authenticated domain. The link should be single-use, tied to the invited address, and expire after a set time (seven days is a common default). Send one reminder at most, and keep promotions out of it.
Below: what to include, a template you can copy (with reminder and expiry versions), the code to send it, and the security details most invite guides skip.
What to include in a team invite email
The person reading it may never have heard of your product. They know the colleague who invited them, and that is the only reason they will trust the email. So lead with the person.
- Who invited them: the inviter's name and email address. Anyone can type "Sarah from Finance" as a display name; the address lets the reader check.
- Where: the workspace or team name, and your product name.
- Their role: "as an Editor" or "as an Admin". It tells them what they will be able to do.
- One button: "Accept invite" or "Join {{workspaceName}}". Nothing else competes with it.
- The expiry date: a real date, not "soon".
- What your product is, in one line, for people who don't know it.
- A way out: "Didn't expect this? You can ignore it. Nothing happens unless you accept."
- The link as text, below the button, for email clients that block buttons.
- An optional personal note from the inviter, if your invite form has one.
Subject lines
Put the inviter's name first. People open email from people they know.
| Good | Weak |
|---|---|
| Sarah Miller invited you to Acme Design on Acme | You've been invited! |
| Join Sarah in Acme Design | Action required: pending invitation |
| Your invite to Acme Design expires on 31 October | Don't miss out on Acme |
The weak ones say nothing about who or where, and the last one reads like an ad. Bento's guide to invitation emails makes the same point with "You're invited!" as its bad example.
Team invite email template
Copy this and replace the {{double braces}}. The names match the code further down.
Subject: {{inviterName}} invited you to {{workspaceName}} on Acme
Preheader (the preview text after the subject): Join as {{roleLabel}}. This invite expires on {{expiresOn}}.
Body:
Hi,
{{inviterName}} ({{inviterEmail}}) invited you to join {{workspaceName}} on Acme as {{roleLabel}}.
"{{note}}" (only if the inviter wrote a note)
[Accept invite]
This invite is for {{inviteeEmail}} and expires on {{expiresOn}}.
Acme is a shared workspace for design reviews. (One line on what your product does.)
Didn't expect this? You can ignore this email. Nobody gets access unless you accept.
Button not working? Paste this link into your browser: {{acceptUrl}}
Reminder (one, after about three days)
Subject: Your invite to {{workspaceName}} expires on {{expiresOn}}
{{inviterName}} invited you to {{workspaceName}} on Acme a few days ago. The invite is still open until {{expiresOn}}.
[Accept invite]
Expired (to the inviter, not the invitee)
Subject: Your invite to {{inviteeEmail}} expired
{{inviteeEmail}} didn't accept your invite to {{workspaceName}} before it expired. You can send a new one from Settings › Members.
Telling the inviter is more useful than a third email to someone who hasn't answered twice. Courier's invite recipe uses the same pattern: reminder on day 3, note to the inviter on day 7.
Send it from your app
With LuneaMail's transactional API, the template lives in your workspace and your app sends the data. Templates use Handlebars: {{workspaceName}} inserts a value, {{#if note}}…{{/if}} shows a block only when the data has it. In the visual builder, a section set to "show only if role is admin" becomes {{#if (eq role "admin")}}.
The template body:
<p>{{inviterName}} ({{inviterEmail}}) invited you to join
<strong>{{workspaceName}}</strong> on Acme as {{roleLabel}}.</p>
{{#if note}}
<blockquote>{{note}}</blockquote>
{{/if}}
{{#if (eq role "admin")}}
<p>As an admin you can invite people and manage billing.</p>
{{/if}}
<p><a href="{{acceptUrl}}">Accept invite</a></p>
<p>This invite is for {{inviteeEmail}} and expires on {{expiresOn}}.
Didn't expect it? Ignore this email. Nothing happens unless you accept.</p>
Creating the invite and sending it (Node.js):
import crypto from "node:crypto";
const APP_URL = "https://app.example.com"; // fixed, never from the request's Host header
const WEEK = 7 * 24 * 60 * 60 * 1000;
export async function inviteMember({ workspace, inviter, email, role, note }) {
const token = crypto.randomBytes(32).toString("base64url");
const expiresAt = new Date(Date.now() + WEEK);
await db.invite.create({
data: {
workspaceId: workspace.id,
email: email.trim().toLowerCase(),
role,
tokenHash: crypto.createHash("sha256").update(token).digest("hex"),
expiresAt,
},
});
const res = await fetch("https://app.luneamail.com/api/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LUNEAMAIL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
templateId: process.env.INVITE_TEMPLATE_ID,
to: email,
subject: "{{inviterName}} invited you to {{workspaceName}} on Acme",
data: {
inviterName: inviter.name,
inviterEmail: inviter.email,
workspaceName: workspace.name,
role,
roleLabel: ROLE_LABELS[role],
note: note || undefined, // optional: only used inside {{#if note}}
inviteeEmail: email,
acceptUrl: `${APP_URL}/invite/${token}`,
expiresOn: expiresAt.toLocaleDateString("en-GB", { day: "numeric", month: "long" }),
},
}),
});
if (res.status !== 202) {
const { error, message } = await res.json();
throw new Error(`${error}: ${message}`); // e.g. SUPPRESSED: this address bounced before
}
}
A success returns 202 with { status: "queued", messageId }. If a required variable is missing, the API refuses with MISSING_VARIABLE instead of sending an invite with an empty button. If the address bounced or reported spam before, it returns SUPPRESSED; show that to the inviter ("We couldn't deliver to this address") rather than failing silently. The full list of errors is in our license key email guide and on the developers page.
The invite link: tokens and link scanners
The accept link is the part that needs care. It gives access to someone's workspace.
- Random and long. Generate the token with a cryptographically secure generator, as OWASP recommends for reset tokens. Don't use IDs or anything guessable.
- Store a hash, not the token. Then a leaked database doesn't leak working invite links.
- Single use, with an expiry. Accepting, revoking or expiring the invite kills the link.
- Build the URL from a fixed base, never from the incoming request's host. OWASP warns about Host header injection for exactly this kind of link.
- Set
Referrer-Policy: no-referreron the accept page, so the token in the URL isn't sent to other sites the page loads. - Don't accept on the first click. Company mail filters open links to check them. Stytch's documentation says "most security scanners will inspect and click on any links present in emails", which can use up a one-time token before the person sees it. Make the link open a page that says "Join Acme Design as Editor" with a button, and accept the invite only when that button is pressed (a POST request, not the page load).
Expiry, reminders, resend and revoke
- Expiry: seven days is the usual default. Bento calls 24 hours too short and 30 days too long; Sequenzy suggests 48 to 72 hours for sensitive products.
- Reminder: one, around day three, only if the invite is still open. Cancel it when the invite is accepted or revoked.
- Resend: create a new token and a new expiry, and invalidate the old link. Then a resent invite is never already expired.
- Revoke: let admins cancel pending invites in the members list. A mistyped address should not hold a working link for a week.
- Expired link page: say it expired, name the inviter, and offer to ask them for a new invite. Don't show a bare error.
From the LuneaMail team: Invites are the reason LuneaMail exists. We needed to send highly customised invite emails from our own product, and there was no way to do it without a lot of research. Gmail meant workarounds that never looked right. A marketing suite like HubSpot meant paying more than the job was worth, many steps for one styled email, and templates that limited what we could do. Product teams we worked with had the same problem: they only wanted to send a few branded emails, and building sending properly, so the emails actually arrive, is a huge undertaking for a small team. An invite should be one template and one API call.
New users and existing users
The same email reaches two kinds of people.
- New to your product: the accept page goes to sign-up with the email already filled in. Ask for as little as possible; they came to see their colleague's work.
- Already has an account: send them to sign in, then straight into the workspace. Don't make them create a second account.
You can handle both in one template with {{#if hasAccount}}Sign in to join{{else}}Create your account to join{{/if}}.
Also decide what happens when someone signed in as ana@gmail.com opens an invite sent to ana@company.com. The safe default is to accept only for the invited address and say so: "This invite is for ana@company.com. Sign in with that address, or ask Sarah to invite this one."
Stop invites being used for spam
Invites let your users send email, with text they control, to any address. That makes them attractive to spammers: sign up, name the workspace "Claim your prize at …", invite a few thousand strangers. Your domain takes the damage.
- Require a verified email before a user can invite anyone.
- Rate-limit invites per user and per workspace, and alert on spikes.
- Limit the length of workspace names and notes.
- Escape user text. Handlebars escapes HTML in
{{note}}by default. Never use the triple-brace form{{{note}}}for text a user typed. - Watch bounces and spam complaints after invite sends. A run of either from one workspace is a signal.
Transactional, not marketing
An invite is app email, not marketing: one message to one person, sent because a teammate asked for it. It needs no marketing consent. In LuneaMail it carries no marketing footer and still reaches people who unsubscribed from your newsletter; addresses that bounced or reported spam are refused.
That only holds while the email stays about the invite. The FTC's CAN-SPAM guide judges a mixed email by its primary purpose: a subject that reads like an ad, or promotional content above the invite, can make it commercial. And the invitee never signed up for anything. Don't add them to your newsletter until they join and opt in.
For delivery, send from your own domain. Gmail requires SPF or DKIM from every sender (DNS records that prove a server may send for your domain). LuneaMail sends from your domain once you add its DKIM records. Click tracking wraps links in a redirect; you can switch it off per workspace if you'd rather keep invite links untouched.
Checklist
- Inviter's name and email, workspace name, role
- One "Accept invite" button, plus the link as text
- Expiry date written out
- "Didn't expect this?" line
- Token random, hashed, single-use, expiring
- Accept on a button press, not on page load
- Existing users go to sign-in, new users to sign-up
- One reminder, then tell the inviter
- Resend issues a new link; admins can revoke
- Invites rate-limited; user text escaped
- Sent from your domain with SPF and DKIM; no promotions
Common mistakes
- "You've been invited!" as the subject, with no name.
- Only the inviter's display name. Easy to fake; show the address.
- Links that never expire, or expire before the reminder arrives.
- Accepting on GET. A mail scanner joins the workspace before your user does.
- Batching invites. Send immediately; the inviter is often waiting.
- Adding invitees to marketing lists. They didn't sign up.
Sending invites with LuneaMail
LuneaMail sends invites, license keys and other app email with one POST /send call, from templates your team designs in the same editor as your campaigns. App email uses the same monthly allowance as campaigns on every plan, including Free; see pricing. If you also sell licenses, our guide to license key emails covers the email that goes out after a purchase.