send email with node.js

How to Send Email with Node.js in 5 Minutes

Last Updated on August 2, 2026 by ian


Sending transactional email from Node.js — password resets, welcome emails, OTP codes — should take minutes, not hours. This guide shows you how to send your first email using the ConvertNow API with native fetch (Node 18+) and Nodemailer for SMTP.

No SDK installation required. Copy, paste, and you’re sending.

What you need

• Node.js 18 or above (for native fetch)

• A free ConvertNow account — convertnow.co

• 5 minutes

Step 1: Get your API key

Sign up at convertnow.co, go to Email API, click + New Key, and copy it. Store it as an environment variable:

export CONVERTNOW_API_KEY=cn_live_YOUR_KEY
# Or add to your .env file:
# CONVERTNOW_API_KEY=cn_live_YOUR_KEY

Step 2: Send your first email

Node 18+ includes native fetch — no packages needed:

// send-email.js
const response = await fetch('https://api.convertnow.co/v1/email/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.CONVERTNOW_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: '[email protected]',
    from: '[email protected]',
    subject: 'Hello from Node.js',
    html: '<h1>It works!</h1><p>Your first email from Node.js.</p>',
    text: 'It works! Your first email from Node.js.',
  }),
});

const data = await response.json();
console.log(data);
// { id: 'msg_abc123', status: 'sent', message: 'Email queued for delivery' }

Step 3: Build a reusable email utility

// lib/email.js
export async function sendEmail({
  to, from, subject, html, text,
  cc, bcc, replyTo, attachments, tags
}) {
  if (!process.env.CONVERTNOW_API_KEY) {
    throw new Error('CONVERTNOW_API_KEY environment variable is not set');
  }

  const response = await fetch('https://api.convertnow.co/v1/email/send', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.CONVERTNOW_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to, from, subject, html, text,
      cc, bcc, reply_to: replyTo,
      attachments, tags
    }),
  });

  const data = await response.json();
  if (!response.ok) throw new Error(data.error);
  return data;
}

Step 4: Common patterns

Welcome email

await sendEmail({
  to: user.email,
  subject: `Welcome to MyApp, ${user.name}!`,
  html: `<h1>Hi ${user.name}!</h1><p>Your account is ready.</p>`,
  tags: ['welcome', 'onboarding']
});

Password reset

await sendEmail({
  to: user.email,
  subject: 'Reset your password',
  html: `<p>Click <a href="${resetUrl}">here</a> to reset. Link expires in 1 hour.</p>`,
  tags: ['password-reset']
});

With CC, BCC, and attachment

import { readFileSync } from 'fs';

await sendEmail({
  to: '[email protected]',
  cc: '[email protected]',
  bcc: ['[email protected]'],
  subject: 'Your invoice',
  html: '<p>Please find your invoice attached.</p>',
  attachments: [{
    filename: 'invoice.pdf',
    content: readFileSync('./invoice.pdf').toString('base64'),
    content_type: 'application/pdf',
  }]
});

Express.js: send email from a route

import express from 'express';
import { sendEmail } from './lib/email.js';

const app = express();
app.use(express.json());

app.post('/signup', async (req, res) => {
  const { name, email } = req.body;

  // Create user in your database...

  await sendEmail({
    to: email,
    subject: `Welcome, ${name}!`,
    html: `<h1>Hi ${name}!</h1><p>Your account is ready.</p>`,
  });

  res.json({ success: true });
});

app.listen(3000);

Next.js: send email from an API route

// app/api/send-welcome/route.js
import { NextResponse } from 'next/server';

export async function POST(request) {
  const { name, email } = await request.json();

  const response = await fetch('https://api.convertnow.co/v1/email/send', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.CONVERTNOW_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: email,
      subject: `Welcome, ${name}!`,
      html: `<h1>Hi ${name}!</h1><p>Your account is ready.</p>`,
    }),
  });

  const data = await response.json();
  return NextResponse.json(data);
}

Nodemailer: use ConvertNow as SMTP relay

If your existing code uses Nodemailer, ConvertNow is a drop-in SMTP replacement:

import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: 'out.convertnow.co',
  port: 465,
  secure: true, // implicit SSL
  auth: {
    user: '[email protected]',
    pass: process.env.CONVERTNOW_API_KEY,
  },
});

await transporter.sendMail({
  from: 'My App <[email protected]>',
  to: '[email protected]',
  subject: 'Hello via Nodemailer',
  html: '<h1>It works!</h1>',
  text: 'It works!',
  // Custom headers also work via Nodemailer SMTP:
  headers: { 'X-Campaign-ID': 'welcome-series' },
});

Handle webhooks for delivery events

Register a webhook endpoint in your dashboard to receive real-time delivery and failure events:

import crypto from 'crypto';
import express from 'express';

app.post('/webhooks/email',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['x-convertnow-signature'];
    const expected = 'sha256=' + crypto
      .createHmac('sha256', process.env.CONVERTNOW_WEBHOOK_SECRET)
      .update(req.body)
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body);
    if (event.event === 'email.failed') {
      console.error('Delivery failed:', event.data.to, event.data.error);
    }
    res.sendStatus(200);
  }
);

Migrating from SendGrid or Resend?

• SendGrid: remove the personalizations wrapper, replace SG. key prefix with cn_live_, flatten content array to html field

• Resend: change the URL from api.resend.com/emails to api.convertnow.co/v1/email/send, replace re_ with cn_live_

Start sending for free — convertnow.co — 10,000 emails/month on the free plan