Tutorial
Send email with Nuxt
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.
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
npm install @emailit/nodeConfigure your API key
Declare private runtime config keys. Keys outside public are only available on the server:
export default defineNuxtConfig({
runtimeConfig: {
emailitApiKey: '',
emailitWebhookSecret: '',
},
});Nuxt fills them from environment variables with the NUXT_ prefix:
NUXT_EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••
NUXT_EMAILIT_WEBHOOK_SECRET=whsec_••••••••Add a small helper. Files in server/utils are auto-imported in server routes:
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:
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:
<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.
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:
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_KEYin your hosting provider’s environment settings rather than shipping a.envfile. - 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.