Send email with Nuxt through a server-side API route—not directly from a Vue component—so your Volanea secret key remains private. This guide gives you a complete Nuxt implementation that reads an API key from runtime configuration and sends a transactional welcome email through Volanea’s REST API.

What you will build

You will create a small Nuxt application endpoint that accepts a recipient address and first name, then sends one HTML transactional email through Volanea. The implementation uses Nuxt server routes and the built-in $fetch helper, so there is no Volanea-specific SDK to install or maintain.

The completed flow looks like this:

  1. A browser, form handler, background job, or another trusted service calls your Nuxt endpoint.
  2. The Nuxt endpoint runs only on the server.
  3. The endpoint reads NUXT_VOLANEA_API_KEY through runtimeConfig.
  4. The endpoint sends a JSON request to https://api.volanea.com/v1/send.
  5. Volanea accepts the transactional message for processing.
  6. Your endpoint returns a minimal success response without exposing the secret key.

Volanea’s single-send endpoint is POST /v1/send; it accepts a message for one recipient or a group of recipients, and Volanea documents secret keys in the sk_… or sk_test_… format. (volanea.com)

This pattern works well for onboarding emails, password-reset notices, receipts, account alerts, team invitations, and other messages triggered by an application event. It is deliberately server-first: an email API key must never be put in client-side Vue code, runtimeConfig.public, a browser network request, or a checked-in .env file.

Requirements before you send

You need the following before running the example:

  • A Nuxt project that can run server routes. This guide uses the standard server/api convention.
  • A Volanea API key. Start with a test key if your account provides one, then change to a live key only after validating the integration.
  • A sender address on a domain configured for sending in Volanea.
  • Node.js and npm.
  • A recipient address you control for testing.

Your sender matters as much as your code. Use an address that belongs to a domain you have configured for Volanea, such as hello@updates.example.com. Do not copy the placeholder sender in this guide unchanged: updates.example.com is only an example domain and is not configured for your account.

Nuxt runtime configuration is designed for this use case. Values declared under runtimeConfig are server-only by default, while values inside runtimeConfig.public are exposed to the client. (nuxt.com) Keep the Volanea key in the private section.

Install Nuxt and project dependencies

Volanea’s REST API works with Nuxt’s built-in server-side fetching support, so you do not need an additional email SDK for this integration. If you are starting from an empty directory, use these exact commands:

npx nuxi@latest init volanea-nuxt-email
cd volanea-nuxt-email
npm install

Start the development server after creating the files in the next sections:

npm run dev

If you already have a Nuxt application, run the dependency installation command from its root instead:

npm install

The example uses Nuxt’s $fetch helper in a server route. Nuxt exposes $fetch through ofetch for use in application and API-route code. (nuxt.com) That means the HTTP client is already part of your Nuxt application; adding a package solely to make this one JSON request would add another dependency without solving a problem.

Configure the Volanea API key securely

Create a .env file at the root of your Nuxt project. Do not commit this file to source control.

NUXT_VOLANEA_API_KEY=sk_test_replace_with_your_volanea_key

Now add the matching private runtime configuration property in nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Server-only. Do not move this inside runtimeConfig.public.
    volaneaApiKey: '',
  },
})

Nuxt maps NUXT_VOLANEA_API_KEY to runtimeConfig.volaneaApiKey at runtime because the environment variable follows Nuxt’s NUXT_ naming convention and the property is declared in runtimeConfig. (nuxt.com) This is more reliable for deployment than reading an arbitrary environment variable directly in application code.

For local development, the Nuxt CLI reads .env files. For a deployed application, set NUXT_VOLANEA_API_KEY in your hosting provider’s encrypted environment-variable settings as well. Nuxt’s documentation notes that .env is read by the CLI during development, build, and generate workflows, but a running built server does not automatically read the file. (nuxt.com) In production, the deployment environment should supply the variable.

Add .env to .gitignore if it is not already there:

.env
.env.*
!.env.example

You can safely commit an .env.example file without a usable credential:

NUXT_VOLANEA_API_KEY=sk_test_replace_me

Complete working Nuxt email example

Create this file structure:

volanea-nuxt-email/
├── .env
├── nuxt.config.ts
└── server/
    └── api/
        └── send-welcome.post.ts

The endpoint below is complete and copy-pasteable. It validates the incoming JSON body, reads the private key from Nuxt runtime configuration, sends JSON to Volanea, and handles upstream failures without returning Volanea’s raw response body to the browser.

// server/api/send-welcome.post.ts
type SendWelcomeBody = {
  to?: string
  firstName?: string
}

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig(event)

  if (!config.volaneaApiKey) {
    throw createError({
      statusCode: 500,
      statusMessage: 'Missing NUXT_VOLANEA_API_KEY server configuration',
    })
  }

  const body = await readBody<SendWelcomeBody>(event)
  const to = body.to?.trim().toLowerCase()
  const firstName = body.firstName?.trim() || 'there'

  if (!to || !/^\S+@\S+\.\S+$/.test(to)) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Provide a valid recipient email address in "to".',
    })
  }

  try {
    const result = await $fetch('https://api.volanea.com/v1/send', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${config.volaneaApiKey}`,
        'Content-Type': 'application/json',
      },
      body: {
        from: 'Acme Updates <hello@updates.example.com>',
        to: [to],
        subject: 'Welcome to Acme',
        html: `
          <!doctype html>
          <html lang="en">
            <body style="font-family: Arial, sans-serif; line-height: 1.5; color: #1f2937;">
              <h1>Welcome, ${escapeHtml(firstName)}!</h1>
              <p>Thanks for creating your Acme account.</p>
              <p>You can now sign in and finish setting up your workspace.</p>
            </body>
          </html>
        `,
        text: `Welcome, ${firstName}! Thanks for creating your Acme account.`,
      },
    })

    return {
      ok: true,
      message: 'Welcome email accepted for sending.',
      result,
    }
  } catch (error: any) {
    console.error('Volanea send failed', {
      status: error?.response?.status,
      statusText: error?.response?.statusText,
      data: error?.data,
    })

    throw createError({
      statusCode: 502,
      statusMessage: 'Email provider request failed',
    })
  }
})

function escapeHtml(value: string) {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

Replace this line before testing:

from: 'Acme Updates <hello@updates.example.com>',

Use a sender address from a domain configured in your Volanea account. The visible display name can be your product or company name; the email address is the part that must be legitimate for your sending setup.

The payload uses both html and text. HTML gives supported email clients a branded layout, while plain text supplies a useful fallback for clients that do not render HTML or for recipients who prefer text-only messages. Keeping both versions semantically aligned also makes transactional content easier to review and troubleshoot.

The escapeHtml function is important because firstName comes from a request body. Without encoding, a value containing markup could alter the HTML email. For emails with several dynamic values, use a template function or a template system that encodes untrusted text by default rather than interpolating raw user input throughout the markup.

Test the endpoint locally

Run Nuxt:

npm run dev

Then call the local server route from a second terminal:

curl -X POST http://localhost:3000/api/send-welcome \
  -H "Content-Type: application/json" \
  -d '{"to":"you@example.com","firstName":"Taylor"}'

A successful request returns JSON shaped like this:

{
  "ok": true,
  "message": "Welcome email accepted for sending.",
  "result": {}
}

The exact object under result is returned by Volanea, so do not build application logic around a guessed response property. Log and inspect the response during development, then explicitly map only the fields your application needs once you have confirmed them in the API reference.

For a real application, do not leave this route publicly usable as a general-purpose email endpoint. A route that accepts arbitrary recipient addresses can be abused to send email at your expense. Tie the route to an authenticated action—for example, a successful account signup handled on the server—or move the send call directly into the server-side signup service.

Why the request belongs in a Nuxt server route

A common integration mistake is putting the Volanea request in a Vue page or component:

// Do not do this in a client-side component.
await $fetch('https://api.volanea.com/v1/send', {
  headers: {
    Authorization: `Bearer ${apiKey}`,
  },
})

That approach risks exposing the key in browser-delivered JavaScript, development tools, logs, or client-side network traffic. It also makes it harder to apply authorization, recipient validation, rate limits, audit logging, and idempotency controls before a message is created.

Instead, treat server/api/send-welcome.post.ts as a boundary between public input and your email provider. The browser sends only the allowed application data to your own endpoint. The Nuxt server adds the secret authorization header after it has validated the request. Private Nuxt runtime configuration is accessible on the server, whereas runtimeConfig.public is serialized for client access. (nuxt.com)

This separation has practical benefits:

  • You can check that the current user is allowed to trigger the email.
  • You can ensure the recipient is the signed-in user rather than an arbitrary address.
  • You can record an application event before attempting delivery.
  • You can suppress duplicate sends caused by retries or double-clicks.
  • You can change email providers or request details without changing client-side code.
  • You can avoid browser CORS limitations because the provider request happens server-to-server.

Sending from an application action instead of a public endpoint

The sample route is useful for testing, but production email is usually initiated from a business event: an order is paid, a user verifies an address, an invoice is generated, or a teammate is invited. The most reliable design places the send call after the event has been committed to your database.

For example, after creating an account, your server-side signup code can call a shared email function. This avoids accepting a recipient address from the browser at all.

// server/utils/sendWelcomeEmail.ts
export async function sendWelcomeEmail(to: string, firstName: string) {
  const config = useRuntimeConfig()

  if (!config.volaneaApiKey) {
    throw new Error('NUXT_VOLANEA_API_KEY is not configured')
  }

  return await $fetch('https://api.volanea.com/v1/send', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${config.volaneaApiKey}`,
      'Content-Type': 'application/json',
    },
    body: {
      from: 'Acme Updates <hello@updates.example.com>',
      to: [to],
      subject: 'Welcome to Acme',
      html: `<h1>Welcome, ${escapeHtml(firstName)}!</h1><p>Your account is ready.</p>`,
      text: `Welcome, ${firstName}! Your account is ready.`,
    },
  })
}

function escapeHtml(value: string) {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

A server route, scheduled task, queue worker, or another server-only handler can then import and call sendWelcomeEmail. Keep the provider-specific request in one place. Centralizing it makes sender changes, error handling, observability, and future template migration much simpler.

Common errors when you send email with Nuxt

Authentication failures

If Volanea rejects the request as unauthorized, check the API key path first:

  1. Confirm .env contains NUXT_VOLANEA_API_KEY, not a key exposed through NUXT_PUBLIC_.
  2. Confirm nuxt.config.ts declares volaneaApiKey inside runtimeConfig.
  3. Restart npm run dev after changing local environment variables.
  4. Confirm your deployment platform has the same variable configured for the running environment.
  5. Check that the Authorization header uses the exact bearer-token format shown in the sample.

Do not print the full key in logs while debugging. If you must confirm that a key is present, log only a boolean such as Boolean(config.volaneaApiKey) or a non-sensitive prefix length.

Wrong Content-Type

Volanea’s REST send endpoint expects JSON when you send a structured message object. The sample explicitly sets:

'Content-Type': 'application/json'

If you switch from $fetch to the native fetch API, remember to serialize the body yourself:

await fetch('https://api.volanea.com/v1/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${config.volaneaApiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'Acme Updates <hello@updates.example.com>',
    to: ['you@example.com'],
    subject: 'Welcome',
    html: '<p>It works.</p>',
    text: 'It works.',
  }),
})

With Nuxt $fetch, pass an object to body as shown in the main example. Do not manually stringify that body unless you intentionally want to manage the encoding yourself.

Missing await

Email sends are asynchronous. This is incorrect when your next operation depends on the request completing:

$fetch('https://api.volanea.com/v1/send', options)
return { ok: true }

Use await so errors enter your try/catch block and the route does not report success before the upstream request has completed:

const result = await $fetch('https://api.volanea.com/v1/send', options)
return { ok: true, result }

If you intentionally dispatch work in the background, use a real queue or job system with retry and monitoring rather than silently dropping an unhandled promise.

API key exposed in client code

Do not use useRuntimeConfig().public.volaneaApiKey; do not define a NUXT_PUBLIC_VOLANEA_API_KEY; and do not call Volanea from browser-rendered code. A secret key is a server credential. Nuxt documents that properties in runtimeConfig.public are available client-side, so this configuration would expose the credential. (nuxt.com)

Sender-domain problems

A syntactically valid from value is not enough. The sender should belong to a domain configured for your Volanea account. If a message is rejected or does not behave as expected, verify that the exact domain in the from address matches your sending-domain configuration. Also check for accidental environment mismatches, such as using a production sender with a test setup.

Invalid recipient input

The regular expression in the example is intentionally lightweight. It catches obvious malformed values but does not prove an inbox exists, receives mail, or belongs to the person entering it. For registration flows, use confirmation mechanisms and consider validating high-value addresses before triggering expensive workflows. You can also use the email address verification tool before adding a contact to a sensitive workflow.

Treating acceptance as inbox delivery

A successful API call means the provider accepted the request for processing; it is not proof that a recipient read the email or that every downstream mailbox decision has completed. Keep application state separate from delivery state. For example, mark a password reset as requested when you create the token, not only when an email is eventually opened.

Production safeguards for transactional email

The copy-paste example proves the integration, but production systems need a few additional controls.

Make retries safe

Networks fail, servers restart, and callers retry. If an action can be retried after the provider receives the request but before your application receives the response, the recipient may get duplicate mail. Avoid solving this with blind retries.

Instead, persist a unique business event before sending. For example, store a welcome_email_sent_at timestamp, an invitation ID, or an outbox row with a unique event key. Only dispatch the email once per key unless the product deliberately supports resending it.

Keep the recipient authoritative

For logged-in flows, derive the recipient from your database instead of trusting a to field posted by the browser. A password-reset handler can accept an email address because its purpose is to locate an account, but an invoice route should read the billing email from the completed order record.

Limit exposed routes

If a route exists only to support an internal operation, protect it with authentication and authorization. Apply rate limits to contact forms and invitation routes. Capture enough structured logging to investigate failures—such as an internal request ID, event type, and recipient-domain hash—without storing sensitive message content unnecessarily.

Build readable messages

Use a clear sender, precise subject, short opening line, and a plain-text alternative. For transactional mail, include only the action-relevant content. Receipts should identify the order; security alerts should identify the event and next action; password resets should include expiry information from your own application logic.

Separate transactional and campaign behavior

A welcome email immediately after signup is transactional. A monthly product newsletter is a campaign. Keep the logic, audience permissions, unsubscribe behavior, and send timing appropriate to the message type. Volanea also provides campaign-oriented API functionality for one-off broadcasts and scheduling, but this Nuxt guide focuses on a single transactional send. (volanea.com)

Next steps: webhooks and templates

Once the first send works, add webhooks so your application can receive lifecycle events from the email system. A webhook endpoint is a server route that receives a provider event, verifies it according to the provider’s webhook security guidance, records the event, and returns a successful response quickly. Use webhooks to update operational records, investigate bounces, measure delivery behavior, or trigger follow-up application workflows. Do not treat a webhook handler as a place for long-running business logic; record the event and enqueue heavier work.

Next, move repeated HTML into reusable templates. Volanea’s template API stores reusable content that can be referenced by a template ID instead of carrying full markup in every send request. (volanea.com) Templates reduce duplication across your Nuxt routes and make it easier to keep email layout, brand language, and fallback text consistent. Keep application data preparation in Nuxt, and keep presentation in the template where that separation fits your workflow.

For additional endpoint details, request fields, and setup guidance, consult the Volanea API reference and setup documentation. As your volume or sending needs grow, also review transactional email sending plans so your production environment has appropriate capacity.

FAQ

Do I need a Volanea Nuxt SDK?

No. This integration uses Volanea’s REST API from a Nuxt server route with the built-in $fetch helper. That avoids relying on a fictional framework-specific SDK and keeps the request format visible in your code.

Can I send email directly from a Nuxt Vue component?

No. Send it from a Nuxt server route, server utility, worker, or another server-only execution path. Calling an email API from a Vue component risks exposing your secret key to the browser.

Which environment variable should I use?

Use NUXT_VOLANEA_API_KEY with runtimeConfig.volaneaApiKey declared in nuxt.config.ts. Do not use a NUXT_PUBLIC_ variable for an email API secret.

Why include both html and text in the email payload?

HTML gives you formatting and branding; plain text provides a usable fallback for text-only clients and recipients. Both should communicate the same transaction and next action.

Can this route send to multiple recipients?

Volanea documents that its single-send endpoint can handle one address or up to 50 recipients. For transactional messages, however, send to the intended person or use a purpose-built batch flow when messages need recipient-specific data. (volanea.com)