Integration

Send email from Nuxt with SNDR.

Send transactional email from Nuxt — server/api routes, Nitro endpoints, useFetch from forms, webhook receivers via readRawBody. Works on Node, Vercel, Netlify, and Cloudflare presets.

Nuxt's server directory + Nitro server engine give you the same Web standards Request/Response your client code already uses. The SDK works unchanged. The recipes below show server/api routes for typed endpoints, Nitro h3 helpers for body handling, and webhook receivers using readRawBody so HMAC verification stays correct.

Setup

Install + configure

Installbash
pnpm add @rkiza/sndr
# or
npm install @rkiza/sndr
# or
yarn add @rkiza/sndr
Environmentenv
# .env
NUXT_SNDR_API_KEY=sndr_live_x9k2...
NUXT_SNDR_WEBHOOK_SECRET=...

# Expose via runtimeConfig in nuxt.config.ts:
# export default defineNuxtConfig({
#   runtimeConfig: {
#     sndrApiKey: '',           // overridden by NUXT_SNDR_API_KEY
#     sndrWebhookSecret: '',    // overridden by NUXT_SNDR_WEBHOOK_SECRET
#   },
# });

Recipes

The patterns you actually need.

  1. Send from a server/api route

    Nuxt's server/api/<path>.ts files become HTTP handlers automatically. defineEventHandler is the Nitro shape; readBody gives you the parsed JSON.

    // server/api/welcome.post.ts
    import { Sndr, SndrError } from '@rkiza/sndr';
    
    export default defineEventHandler(async (event) => {
      const config = useRuntimeConfig();
      const sndr = new Sndr(config.sndrApiKey);
    
      const { email, name } = await readBody<{ email: string; name: string }>(event);
    
      try {
        const { id } = await sndr.emails.send({
          from: 'hello@yourdomain.com',
          to: email,
          subject: `Welcome, ${name}`,
          html: `<p>Thanks for joining us, ${name}.</p>`,
        });
        return { id };
      } catch (err) {
        if (err instanceof SndrError) {
          throw createError({
            statusCode: err.status,
            statusMessage: err.code,
            data: { message: err.message },
          });
        }
        throw err;
      }
    });
  2. Form submission from a Vue page

    useFetch (or $fetch in handlers) is the SSR-aware client. The server handler validates and sends; the page surfaces the result.

    <!-- pages/contact.vue -->
    <script setup lang="ts">
    const form = ref({ name: '', email: '', message: '' });
    const result = ref<{ ok: boolean; error?: string } | null>(null);
    
    async function submit() {
      try {
        await $fetch('/api/contact', { method: 'POST', body: form.value });
        result.value = { ok: true };
      } catch (err: any) {
        result.value = { ok: false, error: err.data?.message ?? 'send failed' };
      }
    }
    </script>
    
    <template>
      <form @submit.prevent="submit">
        <input v-model="form.name" required minlength="2" />
        <input v-model="form.email" type="email" required />
        <textarea v-model="form.message" required minlength="10" />
        <button type="submit">Send</button>
        <p v-if="result?.ok">Message received.</p>
        <p v-if="result?.error">{{ result.error }}</p>
      </form>
    </template>
  3. Webhook receiver — readRawBody

    Nitro's readRawBody helper exposes the raw bytes before any body parsing kicks in — exactly what HMAC verification needs. Don't use readBody here; the parsed-then-re-serialised JSON breaks the signature.

    // server/api/webhooks/sndr.post.ts
    import { verifyWebhookSignature } from '@rkiza/sndr';
    import type { WebhookEventPayload } from '@rkiza/sndr';
    
    export default defineEventHandler(async (event) => {
      const config = useRuntimeConfig();
      const rawString = await readRawBody(event);
      if (!rawString) {
        throw createError({ statusCode: 400, statusMessage: 'empty body' });
      }
      const raw = Buffer.from(rawString, 'utf8');
    
      const ok = verifyWebhookSignature(
        raw,
        getHeader(event, 'x-sndr-signature') ?? '',
        config.sndrWebhookSecret,
      );
      if (!ok) {
        throw createError({ statusCode: 400, statusMessage: 'bad signature' });
      }
    
      const payload = JSON.parse(raw.toString('utf8')) as WebhookEventPayload;
      switch (payload.type) {
        case 'email.delivered':
          // payload.data.email_id, payload.data.to: string[]
          break;
        case 'email.bounced':
          // payload.data.email_id, payload.data.error
          break;
        case 'email.unsubscribed':
          // payload.data.addresses: string[]
          break;
      }
    
      return { ok: true };
    });
  4. Server middleware for shared validation

    If multiple routes need the same auth or rate-limiting, a server/middleware.ts file runs on every request. Keep Sndr instantiation per-route or per-module — middleware is not the right place for it.

    // server/middleware/rate-limit.ts
    const counts = new Map<string, { count: number; reset: number }>();
    
    export default defineEventHandler((event) => {
      if (!event.path.startsWith('/api/')) return;
    
      const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown';
      const now = Date.now();
      const entry = counts.get(ip) ?? { count: 0, reset: now + 60_000 };
    
      if (now > entry.reset) {
        entry.count = 0;
        entry.reset = now + 60_000;
      }
      entry.count += 1;
      counts.set(ip, entry);
    
      if (entry.count > 30) {
        throw createError({ statusCode: 429, statusMessage: 'rate limited' });
      }
    });

Nuxt FAQ

Does the SDK work on every Nuxt deployment preset?

Yes for Node, Vercel, Netlify. For Cloudflare, the send path works; the webhook verifier requires the nodejs_compat flag (same as raw Workers).

Why readRawBody instead of readBody for webhooks?

readBody parses (JSON / FormData / multipart). HMAC must be computed over the exact bytes SNDR signed; re-serialised JSON loses ordering and whitespace. readRawBody returns the raw string.

How do I expose the API key safely?

Use runtimeConfig (not publicRuntimeConfig). Nuxt strips runtimeConfig keys from the client bundle automatically. NUXT_SNDR_API_KEY overrides the empty default at runtime.

Should I queue sends in Nuxt?

For high-volume or critical sends, yes. Pair with a job queue (BullMQ on Redis, or Cloudflare Queues on the CF preset) so transactional sends survive request failures.

Ship your first Nuxt email in 5 minutes.

Free tier includes 3,000 emails / month. No card required.

Start building

Your next message starts here.

Connect your domain, create an API key, and send with SNDR.