Skip to content
Docs

Tutorial

Send email from Node.js with the @emailit/node SDK or Nodemailer over SMTP, and verify Emailit webhooks in an Express route.

Updated Oct 1, 2026

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

Terminal
npm install @emailit/node

The 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:

.env
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:

send.mjs
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:

server.mjs
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:

JavaScript
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:

mailer.mjs
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():

webhooks.mjs
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: 2 keeps a pooled transport at 2 messages per second. See Limits.
  • Make retries safe. If you retry a send after a timeout, send an Idempotency-Key header so Emailit doesn’t send twice. The SDK doesn’t set custom headers, so use fetch for those calls; see Idempotency.
  • Process webhooks idempotently. Store each event_id you’ve handled and skip duplicates, and do slow work in a background job after you return 200.
  • Use TypeScript? The SDK has no type declarations yet. Add declare module '@emailit/node'; to a .d.ts file in your project.

Next steps

Attachments, scheduling, tracking and metadata.
Every event and its payload.
Route handlers and server actions.
All official libraries.

Was this page helpful?

Thanks for the feedback.

Thanks, we read every message.