# 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](/docs/domains/add-a-domain/), for example `acme.com`.
- An [API key](/docs/developers/api-keys/). A Sending Only key is enough to send.
- Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members. Use your own address while you test.

## Install the SDK

```bash
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:

```bash title=".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:

```javascript title="send.mjs"

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 API → Emails** within seconds.

In an Express app, send from the route that triggers the email:

```javascript title="server.mjs"

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](/docs/templates/) and the full list of fields in [Send an email](/docs/api-reference/emails/send/).

## Handle errors

The SDK throws typed errors, so you can react to each failure:

```javascript
  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](https://nodemailer.com), point it at the Emailit SMTP relay. Install it with `npm install nodemailer`:

```javascript title="mailer.mjs"

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](/docs/smtp/settings/).

## Receive webhooks

[Create a webhook](/docs/webhooks/set-up/) 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()`:

```javascript title="webhooks.mjs"

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);
}

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](/docs/webhooks/request-signature/) and [Retries and failures](/docs/webhooks/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](/docs/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](/docs/email-api/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

  - [Send email with the API](/docs/email-api/send-email/): Attachments, scheduling, tracking and metadata.
  - [Webhook event types](/docs/webhooks/event-types/): Every event and its payload.
  - [Next.js](/docs/frameworks/nextjs/): Route handlers and server actions.
  - [SDKs and libraries](/docs/sdks/): All official libraries.

---
Source: https://emailit.com/docs/frameworks/nodejs/
