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

