Messy Founder
Resource

How to Send Onboarding Emails That Aren’t a Newsletter

Sam Ortega•

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 onboardingNewsletter / marketing
“Welcome — here’s your dashboard”Weekly tips campaign
“Verify your email”Product update blast
Triggered by signup eventTriggered by segment membership
Same pipe as password resetsSeparate 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

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

Testing the welcome path

  1. Create a user in staging
  2. Confirm welcome_email_sent_at is null
  3. Trigger signup / call sendWelcomeEmail
  4. Check Notify email logs
  5. 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.

Resources

Explore More

Discover more resources

Browse y/our curated collection of tools, guides, and resources to help you build, grow, and scale.

Browse Blog

Share Your Story

Have a story to tell?

Join the network and share your journey. Your experiences can inspire and help others on their path.

Share Your Story