
How to Send Onboarding Emails That Aren’t a Newsletter
Onboarding email should be one (maybe two) transactional messages: welcome and verify your email. It is not a five-step drip, not a nurture sequence, and not a reason to buy a marketing ESP.
This guide shows a single post-signup send with Notify. You own the HTML; Notify delivers over POST https://notify.cx/api/email/send with an x-api-key header. Same pipe as password resets and receipts — deliberately not a newsletter platform.
Transactional vs newsletter (draw the line)
| Transactional onboarding | Newsletter / marketing |
|---|---|
| “Welcome — here’s your dashboard” | Weekly tips campaign |
| “Verify your email” | Product update blast |
| Triggered by signup event | Triggered by segment membership |
| Same pipe as password resets | Separate reputation recommended |
If product wants a drip later, that’s a different tool and preferably a different subdomain. Don’t contaminate reset reputation with campaign volume.
When to send
User created → (optional) verify email
→ welcome email (once)
Prefer sending after the account row commits. Gate with welcome_email_sent_at (or equivalent) so retries and double-clicks don’t spam.
Prerequisites
- Server-side runtime (Next.js Route Handler / Server Action, Express, Worker, etc.)
- Notify API key
- Verified domain for production
from
NOTIFY_API_KEY=your_api_key_here
NEXT_PUBLIC_APP_URL=https://yourapp.com
While iterating on HTML, use the non-delivering sandbox: POST https://notify.cx/api/email/send/test. Details: sandbox vs production.
Send helper
// lib/email.ts
export async function sendEmail(opts: {
to: string;
subject: string;
message: string;
}) {
const apiKey = process.env.NOTIFY_API_KEY;
if (!apiKey) throw new Error('NOTIFY_API_KEY is not set');
const res = await fetch('https://notify.cx/api/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey
},
body: JSON.stringify({
from: 'hey@your-verified-domain.com',
...opts
})
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
Idempotency stubs
// lib/db/onboarding.ts
// Implement against your users table.
export async function alreadySentWelcome(email: string): Promise<boolean> {
// SELECT welcome_email_sent_at IS NOT NULL FROM users WHERE email = …
throw new Error('Implement alreadySentWelcome');
}
export async function markWelcomeSent(email: string): Promise<void> {
// UPDATE users SET welcome_email_sent_at = now() WHERE email = …
throw new Error('Implement markWelcomeSent');
}
Welcome email
import { sendEmail } from '@/lib/email';
import { alreadySentWelcome, markWelcomeSent } from '@/lib/db/onboarding';
async function sendWelcomeEmail(user: { email: string; name?: string }) {
if (await alreadySentWelcome(user.email)) return;
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
await sendEmail({
to: user.email,
subject: 'Welcome to Example',
message: `
<h1>Welcome${user.name ? `, ${user.name}` : ''}</h1>
<p>You’re in. Here’s the only link you need:</p>
<p><a href="${appUrl}/dashboard">Open your dashboard</a></p>
<p>If you did not create this account, ignore this email.</p>
`
});
await markWelcomeSent(user.email);
}
Call this from your signup path after the user insert succeeds:
// Example: inside a Server Action or POST /api/signup handler
const user = await createUser({ email, name, passwordHash });
await sendWelcomeEmail({ email: user.email, name: user.name });
Pair with verification (still not a drip)
If you require verified email, send verify first (or combine carefully into one message with one primary CTA). The verification token pattern lives in magic link / OTP docs.
Avoid: Day 0 welcome, Day 1 tip, Day 3 tip, Day 7 tip on the transactional domain. That sequence belongs on a marketing subdomain with its own reputation, if you need it at all.
Copy rules
- One primary CTA
- Product name in the subject
- No urgency spam (“ACT NOW!!!”)
- Plain “you signed up for X” context
- Optional: link to docs or getting-started — still one email, not a series
Where to hook
- After
INSERTon users (Next.js Server Action / route) - Auth provider callback (auth verification recipe)
- Supabase Edge Function on profile create (Supabase + Notify)
Testing the welcome path
- Create a user in staging
- Confirm
welcome_email_sent_atis null - Trigger signup / call
sendWelcomeEmail - Check Notify email logs
- Trigger again — second send should no-op via the idempotency stub
Use POST https://notify.cx/api/email/send/test while iterating on HTML copy (sandbox vs production).
Observability
When signup volume grows, watch delivery in logs. On Pro/Scale, add webhooks so hard bounces stop future welcome retries.
Notify pricing for context: Free 1,000 emails/mo, Pro $10 / 10,000, Scale $50 / 100,000 — see pricing.
Bottom line
Onboarding email is a one-shot transactional send. Keep brand voice in HTML you own. Use Notify (or any thin transactional API) — not a newsletter platform. Start at notify.cx if you want the small API surface.
