Tutorial
Send email with Node.js
Send email from Node.js with the @emailit/node SDK or Nodemailer over SMTP, and verify Emailit webhooks in an Express route.
This guide shows how to send email from a Node.js app with the official @emailit/node SDK, how to use Nodemailer over SMTP instead, and how to receive signed webhooks in Express.
Prerequisites
- Node.js 18 or later.
- A verified sending domain, for example
acme.com. - An API key. A Sending Only key is enough to send.
- Until your workspace has production access, you can only send to the account emails of workspace members. Use your own address while you test.
Install the SDK
npm install @emailit/nodeThe package is ESM-only. Use import syntax, or await import('@emailit/node') from CommonJS code.
Configure your API key
Keep the key out of your code. Put it in an environment variable:
EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••Load the file with node --env-file=.env (Node.js 20.6 and later), a package such as dotenv, or your platform’s secret settings. Add .env to .gitignore.
Send an email
Create one client and reuse it across requests:
import { Emailit } from '@emailit/node';
const emailit = new Emailit(process.env.EMAILIT_API_KEY);
const email = await emailit.emails.send({
from: 'Acme <hello@acme.com>',
to: 'ada@example.com',
subject: 'Welcome to Acme',
html: '<p>Thanks for signing up.</p>',
text: 'Thanks for signing up.',
});
console.log(email.id); // em_…Run it with node --env-file=.env send.mjs. The email appears in Email APIEmails within seconds.
In an Express app, send from the route that triggers the email:
import express from 'express';
import { Emailit } from '@emailit/node';
const app = express();
const emailit = new Emailit(process.env.EMAILIT_API_KEY);
app.post('/signup', express.json(), async (req, res) => {
const { email } = req.body;
// Create the user here, then send the welcome email.
const sent = await emailit.emails.send({
from: 'Acme <hello@acme.com>',
to: email,
template: 'welcome',
variables: { email },
});
res.status(201).json({ emailId: sent.id });
});
app.listen(3000);template takes a template alias or tem_ ID, and variables fills it in. See Templates and the full list of fields in Send an email.
Handle errors
The SDK throws typed errors, so you can react to each failure:
import {
AuthenticationException,
RateLimitException,
UnprocessableEntityException,
ApiErrorException,
} from '@emailit/node';
try {
await emailit.emails.send(message);
} catch (err) {
if (err instanceof RateLimitException) {
// 429: wait err.jsonBody.retry_after seconds, then retry
} else if (err instanceof AuthenticationException) {
// 401: the API key is missing or invalid
} else if (err instanceof UnprocessableEntityException) {
// 422: for example, the from domain isn't verified
} else if (err instanceof ApiErrorException) {
console.error(err.httpStatus, err.jsonBody);
} else {
throw err;
}
}Send with SMTP instead
If your app already uses Nodemailer, point it at the Emailit SMTP relay. Install it with npm install nodemailer:
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: 'smtp.emailit.com',
port: 587,
secure: false, // upgraded with STARTTLS after connecting
requireTLS: true,
auth: {
user: 'emailit',
pass: process.env.EMAILIT_API_KEY,
},
});
const info = await transporter.sendMail({
from: 'Acme <hello@acme.com>',
to: 'ada@example.com',
subject: 'Welcome to Acme',
text: 'Thanks for signing up.',
html: '<p>Thanks for signing up.</p>',
});
console.log(info.response); // 250 2.0.0 OK: queued as em_…For port 465, set port: 465 and secure: true. If your network blocks 587, use 2525 or 2587 with the same settings. See SMTP settings.
Receive webhooks
Create a webhook that points to https://your-app.com/webhooks/emailit and copy its signing secret (whsec_…) into EMAILIT_WEBHOOK_SECRET.
Emailit signs each request with an HMAC-SHA256 of timestamp.rawBody, so the route must read the raw body. Use express.raw() on this route, not express.json():
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyEmailitSignature(rawBody, signature, timestamp, secret) {
if (!signature || !timestamp) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false; // reject replays older than 5 minutes
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}
export const webhooks = express.Router();
webhooks.post('/webhooks/emailit', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
const valid = verifyEmailitSignature(
rawBody,
req.get('x-emailit-signature'),
req.get('x-emailit-timestamp'),
process.env.EMAILIT_WEBHOOK_SECRET,
);
if (!valid) return res.status(401).send('Invalid signature');
// The body is an array of up to 100 events.
const events = JSON.parse(rawBody);
for (const event of events) {
switch (event.type) {
case 'email.delivered':
// event.data.object.id is the em_ ID
break;
case 'email.bounced':
case 'email.complained':
// stop emailing event.data.object.to
break;
}
}
res.sendStatus(200);
});Mount the router with app.use(webhooks) before any global express.json() middleware. Return a 2xx within 30 seconds; anything else is retried. Read Request signature and Retries and failures for details.
Production tips
- Scope the key. Use a Sending Only key restricted to your sending domain for the app server. Keep Full Access keys for admin scripts.
- Stay under your rate limit. New workspaces can send 2 emails per second and 5,000 per day by default, shared between the API and SMTP. Send bulk jobs from a queue. With Nodemailer,
pool: true, rateLimit: 2keeps a pooled transport at 2 messages per second. See Limits. - Make retries safe. If you retry a send after a timeout, send an
Idempotency-Keyheader so Emailit doesn’t send twice. The SDK doesn’t set custom headers, so usefetchfor those calls; see Idempotency. - Process webhooks idempotently. Store each
event_idyou’ve handled and skip duplicates, and do slow work in a background job after you return200. - Use TypeScript? The SDK has no type declarations yet. Add
declare module '@emailit/node';to a.d.tsfile in your project.