Skip to content
Docs

Tutorial

Send email from a Nuxt server API route with the Emailit Node.js SDK, keep the key in private runtimeConfig and verify webhooks with h3.

Updated Oct 1, 2026

This guide shows how to send email from a Nuxt 3 app. You call Emailit from a server API route, keep the API key in private runtimeConfig, and verify webhooks with h3 helpers.

Prerequisites

  • Nuxt 3 with a Node.js 18+ server preset.
  • A verified sending domain, for example acme.com.
  • An API key. A Sending Only key restricted to your domain is enough.
  • Until your workspace has production access, you can only send to the account emails of workspace members.

Install the SDK

Terminal
npm install @emailit/node

Configure your API key

Declare private runtime config keys. Keys outside public are only available on the server:

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    emailitApiKey: '',
    emailitWebhookSecret: '',
  },
});

Nuxt fills them from environment variables with the NUXT_ prefix:

.env
NUXT_EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••
NUXT_EMAILIT_WEBHOOK_SECRET=whsec_••••••••

Add a small helper. Files in server/utils are auto-imported in server routes:

server/utils/emailit.ts
import type { H3Event } from 'h3';
import { Emailit } from '@emailit/node';

export function emailitClient(event: H3Event) {
  return new Emailit(useRuntimeConfig(event).emailitApiKey);
}

If TypeScript reports a missing declaration for @emailit/node, add declare module '@emailit/node'; to a .d.ts file in your project.

Send an email

Create a server API route. This contact form route sends to a fixed internal address, so visitors can’t use it to email arbitrary people:

server/api/contact.post.ts
export default defineEventHandler(async (event) => {
  const { email, message } = await readBody(event);

  if (typeof email !== 'string' || typeof message !== 'string' || !email.includes('@')) {
    throw createError({ statusCode: 400, statusMessage: 'Invalid input' });
  }

  const sent = await emailitClient(event).emails.send({
    from: 'Acme website <website@acme.com>',
    to: 'support@acme.com',
    reply_to: email,
    subject: 'New contact form message',
    text: message,
  });

  return { id: sent.id };
});

Call it from a page or component with $fetch:

pages/contact.vue
<script setup lang="ts">
const email = ref('');
const message = ref('');
const sent = ref(false);

async function submit() {
  await $fetch('/api/contact', {
    method: 'POST',
    body: { email: email.value, message: message.value },
  });
  sent.value = true;
}
</script>

<template>
  <form @submit.prevent="submit">
    <input v-model="email" type="email" required />
    <textarea v-model="message" required />
    <button type="submit">Send</button>
    <p v-if="sent">Thanks, we'll be in touch.</p>
  </form>
</template>

To send a stored template instead of inline content, pass template (an alias or tem_ ID) and variables. See Templates and Send an email.

Send with SMTP instead

If you prefer SMTP, use Nodemailer in a server route. SMTP needs a Node.js server preset; edge and worker presets can’t open SMTP connections, so use the API there.

server/utils/mailer.ts
import nodemailer from 'nodemailer';

export function mailer() {
  return nodemailer.createTransport({
    host: 'smtp.emailit.com',
    port: 587,
    secure: false,
    requireTLS: true,
    auth: { user: 'emailit', pass: useRuntimeConfig().emailitApiKey },
  });
}

Then call await mailer().sendMail({ from, to, subject, html }) from a route. See SMTP settings for other ports.

Receive webhooks

Create a webhook that points to https://your-app.com/api/webhooks/emailit. Verify the signature against the raw body, then parse the array of events:

server/api/webhooks/emailit.post.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

export default defineEventHandler(async (event) => {
  const rawBody = (await readRawBody(event)) ?? '';
  const signature = getHeader(event, 'x-emailit-signature') ?? '';
  const timestamp = getHeader(event, 'x-emailit-timestamp') ?? '';
  const secret = useRuntimeConfig(event).emailitWebhookSecret;

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const valid =
    Number.isFinite(age) &&
    age <= 300 &&
    expected.length === signature.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

  if (!valid) {
    throw createError({ statusCode: 401, statusMessage: 'Invalid signature' });
  }

  const events = JSON.parse(rawBody) as Array<{ event_id: string; type: string; data: { object: any } }>;
  for (const item of events) {
    if (item.type === 'email.complained') {
      // Stop emailing item.data.object.to
    }
  }

  return { received: events.length };
});

Return a 2xx within 30 seconds; other responses are retried. See Request signature.

Production tips

  • Set secrets in your host. Configure NUXT_EMAILIT_API_KEY in your hosting provider’s environment settings rather than shipping a .env file.
  • Validate before you send. Only accept a recipient from the browser when it’s the signed-in user’s own address, and rate-limit public routes. Every email costs credits and counts against your sending limits.
  • Use separate keys per environment so you can revoke a preview key without touching production.

Next steps

Error handling and Nodemailer details.
Attachments, scheduling and tracking.
Every event and its payload.
Scopes, domain restrictions and rotation.

Was this page helpful?

Thanks for the feedback.

Thanks, we read every message.