Sending email with SvelteKit should happen on the server, where your Volanea API key can remain private and your application can handle API failures safely. This guide provides a complete, copy-pasteable SvelteKit implementation that sends one transactional email through Volanea’s REST API.

What you will build

You will create a small SvelteKit page with a form and a server action. When the form is submitted, the action validates a recipient address, reads VOLANEA_API_KEY from the server environment, and makes a POST request to Volanea’s POST /v1/send endpoint.

The example deliberately uses the platform fetch API rather than an unverified provider-specific SDK. That keeps the integration portable, avoids adding a dependency solely for one HTTP request, and maps directly to Volanea’s documented REST sending endpoint. Volanea accepts secret keys beginning with sk_ or sk_test_; test keys are useful while wiring up an integration because they render and log messages without delivering them.

By the end, you will have:

  • A server-only Volanea API key stored in .env.
  • A SvelteKit form that posts to a server action.
  • A native fetch request to https://api.volanea.com/v1/send.
  • JSON request headers, including bearer authentication.
  • Useful error handling for authentication, invalid input, malformed JSON, and upstream failures.
  • A foundation for adding reusable templates, delivery-event webhooks, and idempotent retries.

This is a transactional-email pattern. It suits messages caused by an individual application event, such as a welcome email, receipt, account alert, password-reset notification, or invite. It is not a replacement for a marketing-campaign workflow where recipients, consent, unsubscribe behavior, scheduling, and audience segmentation require different application logic.

Prerequisites

Before you send email with SvelteKit, make sure you have the following in place:

  1. A SvelteKit application running on a supported server runtime.
  2. Node.js installed locally. Modern SvelteKit deployments run on a current Node runtime with a global fetch implementation available server-side.
  3. A Volanea account, a secret API key, and a sender address that your Volanea account is allowed to use.
  4. A recipient address you control for testing.
  5. A domain authentication setup appropriate for production sending. Authentication is separate from application code: your application sends the request, while DNS records establish authorization for the sending domain.

Do not put a secret Volanea key in a PUBLIC_ environment variable. In SvelteKit, public variables can be included in browser-visible code. The integration below imports from $env/dynamic/private, which is server-only and must only be used in server modules such as +page.server.ts, +server.ts, hooks, or other backend code.

For local work, create a .env file at the root of your project. Keep it out of version control. If your repository does not already ignore .env, add it to .gitignore before adding credentials.

# .gitignore
.env
.env.*
!.env.example

Use an example file to document required configuration without committing the actual key:

# .env.example
VOLANEA_API_KEY=sk_test_replace_me
VOLANEA_FROM_EMAIL=hello@your-verified-domain.com

Your actual local file contains your real test or live key:

# .env
VOLANEA_API_KEY=sk_test_your_actual_secret_key
VOLANEA_FROM_EMAIL=hello@your-verified-domain.com

A test key and a live key should be treated as secrets. Do not paste either into client-side code, public issue trackers, screenshots, or browser network requests. Store the same values in your deployment provider’s encrypted environment-variable settings when you deploy.

Install the SvelteKit project dependencies

This guide uses native fetch, so there is no Volanea-specific SvelteKit package to install and no fictional SDK method to learn. The only install command required for the copy-pasteable sample is the normal dependency installation for your SvelteKit project:

npm install

Run the development server after adding the files shown below:

npm run dev

If you are starting from scratch rather than adding this to an existing project, create a SvelteKit application first, then install its dependencies:

npx sv create volanea-sveltekit-email
cd volanea-sveltekit-email
npm install
npm run dev

Native fetch is a good fit for one REST endpoint because it avoids an unnecessary abstraction. The important integration details are visible in your source: the URL, HTTP method, authorization header, JSON content type, request body, response parsing, and failure path. If you later centralize email handling, you can move the fetch call into a server-only module without changing the API contract.

Configure Volanea environment variables

SvelteKit offers static and dynamic environment imports. For credentials used by server actions, use the private dynamic module:

import { env } from '$env/dynamic/private';

$env/dynamic/private makes variables available only while server code executes. It is a sound choice for an API key because the value comes from the server environment at runtime. Do not import this module from a .svelte component or any module that can be bundled for the client.

This guide uses two values:

VariablePurpose
VOLANEA_API_KEYSecret key used in the bearer authorization header.
VOLANEA_FROM_EMAILA sender address on a domain configured for your Volanea account.

The from address is intentionally loaded from the environment rather than accepted from the browser form. Letting a user-controlled request select arbitrary sender addresses creates a security and deliverability problem. Your server should choose the sender identity.

The recipient is entered in the form only to make the example easy to test. In a production workflow, recipients normally come from trusted application data: the authenticated user record, an order’s billing contact, an invite record, or a password-reset request. Never use this sample as a reason to turn an unauthenticated endpoint into an open email relay.

Complete SvelteKit email example

Create or replace src/routes/+page.server.ts with the following server action. It imports private runtime environment variables, validates input, calls Volanea’s REST endpoint, and returns a result the page can display.

// src/routes/+page.server.ts
import { fail } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import type { Actions } from './$types';

const VOLANEA_SEND_URL = 'https://api.volanea.com/v1/send';

function isValidEmail(value: string) {
  // Basic UX validation only. The email provider remains the authority on
  // whether an address can receive mail.
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

export const actions = {
  default: async ({ request, fetch }) => {
    const formData = await request.formData();
    const to = String(formData.get('to') ?? '').trim();

    if (!isValidEmail(to)) {
      return fail(400, {
        success: false,
        message: 'Enter a valid recipient email address.',
        to
      });
    }

    if (!env.VOLANEA_API_KEY) {
      console.error('VOLANEA_API_KEY is not configured.');
      return fail(500, {
        success: false,
        message: 'Email is not configured on this server.',
        to
      });
    }

    if (!env.VOLANEA_FROM_EMAIL) {
      console.error('VOLANEA_FROM_EMAIL is not configured.');
      return fail(500, {
        success: false,
        message: 'A sender address is not configured on this server.',
        to
      });
    }

    const payload = {
      from: env.VOLANEA_FROM_EMAIL,
      to,
      subject: 'Your SvelteKit test email',
      html: `
        <!doctype html>
        <html lang="en">
          <body style="font-family: Arial, sans-serif; line-height: 1.5">
            <h1>It worked</h1>
            <p>This transactional email was sent from a SvelteKit server action.</p>
          </body>
        </html>
      `,
      text: 'It worked. This transactional email was sent from a SvelteKit server action.'
    };

    let response: Response;

    try {
      response = await fetch(VOLANEA_SEND_URL, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${env.VOLANEA_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(payload)
      });
    } catch (error) {
      console.error('Could not reach the Volanea API.', error);

      return fail(502, {
        success: false,
        message: 'The email service could not be reached. Try again shortly.',
        to
      });
    }

    const responseText = await response.text();
    let responseBody: unknown = null;

    try {
      responseBody = responseText ? JSON.parse(responseText) : null;
    } catch {
      // Keep the raw body out of the user-facing response. It may be HTML
      // from a proxy or a non-JSON upstream error response.
      console.error('Volanea returned a non-JSON response:', responseText);
    }

    if (!response.ok) {
      console.error('Volanea send failed:', {
        status: response.status,
        body: responseBody
      });

      return fail(response.status >= 400 && response.status < 600 ? response.status : 502, {
        success: false,
        message: 'Volanea did not accept the email. Check server logs for details.',
        to
      });
    }

    console.info('Volanea accepted email request:', responseBody);

    return {
      success: true,
      message: `Email request accepted for ${to}.`,
      to
    };
  }
} satisfies Actions;

Then create or replace src/routes/+page.svelte with this form UI:

<!-- src/routes/+page.svelte -->
<script lang="ts">
  export let form:
    | {
        success?: boolean;
        message?: string;
        to?: string;
      }
    | undefined;
</script>

<svelte:head>
  <title>Send a Volanea test email</title>
  <meta
    name="description"
    content="Send a transactional test email from a SvelteKit server action."
  />
</svelte:head>

<main>
  <h1>Send a test email</h1>
  <p>Submit the form to send one transactional message through Volanea.</p>

  <form method="POST">
    <label for="to">Recipient email</label>
    <input
      id="to"
      name="to"
      type="email"
      autocomplete="email"
      required
      value={form?.to ?? ''}
      placeholder="you@example.com"
    />

    <button type="submit">Send email</button>
  </form>

  {#if form?.message}
    <p role="status" aria-live="polite">
      {form.message}
    </p>
  {/if}
</main>

<style>
  main {
    max-width: 42rem;
    margin: 3rem auto;
    padding: 0 1rem;
    font-family: system-ui, sans-serif;
  }

  form {
    display: grid;
    gap: 0.75rem;
    margin-top: 1.5rem;
  }

  input,
  button {
    font: inherit;
    padding: 0.7rem;
  }

  button {
    cursor: pointer;
  }
</style>

Start the application, open the local URL printed by SvelteKit, enter an inbox you control, and submit the form. The browser submits to the same route, but the actions.default function runs on the server. The API key is never placed in the HTML, JavaScript bundle, or browser request.

Understand the Volanea request

The server action sends a JSON POST request to Volanea’s send endpoint. The essential request is intentionally small:

const response = await fetch('https://api.volanea.com/v1/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${env.VOLANEA_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    from: env.VOLANEA_FROM_EMAIL,
    to: 'recipient@example.com',
    subject: 'Your SvelteKit test email',
    html: '<h1>It worked</h1>',
    text: 'It worked'
  })
});

Each part has a distinct job:

  • method: 'POST' creates a send request.
  • Authorization carries the secret server credential using bearer authentication.
  • Content-Type: application/json tells the API how to parse the request body.
  • JSON.stringify(...) transforms the JavaScript object into a JSON string.
  • from identifies the sender address your account is configured to use.
  • to identifies the recipient.
  • subject is the message subject line.
  • html supplies the HTML version of the message.
  • text provides a plain-text alternative for recipients and clients that cannot or should not render HTML.

Volanea’s send endpoint can send one message to one address or to multiple addresses, up to 50 recipients in one send request. For this guide, one recipient makes the behavior clear and avoids accidentally turning a form submission into a bulk-send operation. Keep transactional messages narrow and event-driven; use explicit recipient data and authorization checks before any send.

The endpoint performs work beyond merely accepting markup: Volanea documents suppression checks, contact upserts, template rendering, tracking instrumentation, and dispatch as part of its sending pipeline. An accepted request therefore does not mean a message has necessarily reached an inbox. Inbox delivery can still be affected by sender-domain authentication, recipient-server policy, bounces, complaints, suppressions, and the message’s content.

Why the code belongs in a server action

A common integration mistake is trying to call an email API directly from a Svelte component. That would expose your secret key to every visitor who loads the page. Anyone could inspect the browser request or JavaScript bundle, copy the key, and send mail using your account.

A SvelteKit server action is a suitable boundary because it receives form data on the server and can access $env/dynamic/private. The browser knows only that it submitted a form; it never receives the Volanea key.

Server actions also provide a useful place for application rules before a message is sent. For example, a real receipt email action might:

  1. Authenticate the current user or verify a signed payment webhook.
  2. Read the order from the database.
  3. Confirm the order is paid and has not already triggered a receipt.
  4. Get the destination address from the stored order record.
  5. Build the email from trusted order data.
  6. Send the request to Volanea.
  7. Persist the provider response or an internal send record.

That sequence is safer than accepting from, subject, html, and to directly from arbitrary browser form fields. Browser inputs should be considered untrusted until the server validates and authorizes them.

Test the integration safely

Use a Volanea test key while building the integration when possible. Test credentials let you exercise request construction and server-side handling without delivering a message to a real inbox. Once the action works, switch the deployment environment—not your source code—to the appropriate live key.

Test more than the happy path. A dependable email integration has at least these checks:

  • Submit a valid recipient address and confirm the server returns a success message.
  • Submit an invalid recipient string and confirm the action returns a 400-level form error before calling the API.
  • Temporarily remove VOLANEA_API_KEY and confirm the server returns a configuration error without leaking secrets.
  • Use an intentionally invalid key in a local-only environment and confirm authentication failures are logged server-side.
  • Inspect your deployment logs for the API status and sanitized response information.
  • Test the actual sender domain and a real inbox before relying on the integration for customer-critical messages.

The sample logs upstream response data only to the server console. Keep that habit. Do not return provider error bodies directly to an end user: they may contain implementation details that are useful for debugging but confusing or sensitive in a public UI.

When you move from a single test message to business-critical mail, add application-level observability. Record an internal event such as receipt_email_requested, include the relevant order or user ID, and correlate that event with Volanea’s send response. This is much more useful than trying to infer application behavior from an inbox alone.

Add safe retries with an idempotency key

Network failures are ambiguous. Your server can time out after Volanea receives a request but before your application receives the response. Blindly retrying a transactional send can then create duplicate receipts, alerts, or invites.

Volanea documents support for an Idempotency-Key header on POST /v1/send. Generate one stable key for a single business event, save it with the event in your database, and reuse that same key only if you retry the same send. Generate a new key for a genuinely new email event.

Here is the header addition:

headers: {
  Authorization: `Bearer ${env.VOLANEA_API_KEY}`,
  'Content-Type': 'application/json',
  'Idempotency-Key': emailEventId
}

emailEventId should be a server-generated value associated with the specific action, not an email address or another public identifier. For example, a database record for order 1234 might create a random UUID when the receipt workflow begins. If the worker retries after a timeout, it loads the same UUID and uses it again. If the customer creates a new order later, the new order receives a new event ID.

Do not create a random idempotency key inside every retry attempt. That defeats the point: each retry would look like a brand-new operation. Also avoid using an idempotency key as a substitute for database state. Your application should still record whether the underlying business event has been processed and make deliberate decisions about re-sending.

Common errors when sending email with SvelteKit

Authentication failures

If Volanea rejects the request because credentials are missing or invalid, first check that VOLANEA_API_KEY exists in the environment where the server is actually running. A local .env file does not automatically configure a cloud deployment, and changing a deployment environment variable may require a new deployment or server restart depending on the platform.

Confirm that the header is exactly formatted as bearer authentication:

Authorization: `Bearer ${env.VOLANEA_API_KEY}`

Do not send the key in the JSON body, in a query string, or from browser JavaScript. Remove accidental whitespace around copied keys. Use a test key for test mode and the correct production credential for live sending.

Wrong Content-Type or non-JSON request body

Volanea’s REST API expects JSON for this send request. Set the header and serialize the object:

headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)

A frequent bug is passing an object directly as body. Native fetch does not automatically convert a plain JavaScript object into JSON. Another mistake is using FormData while claiming the content is JSON. Either mismatch can cause the API to reject or misread the request.

Forgetting await

Both request.formData() and fetch() are asynchronous. Omitting await means your code gets a promise instead of the resolved data or response.

const formData = await request.formData();
const response = await fetch(VOLANEA_SEND_URL, options);

The same applies to reading a response body. Use await response.text() or await response.json() before inspecting its contents. The example uses text() first and then attempts JSON parsing so it can handle a non-JSON upstream response without throwing a second error.

Using private environment variables in +page.svelte

Do not import $env/dynamic/private in src/routes/+page.svelte. Components can run in the browser, and private environment variables are restricted to server-side modules for a reason.

Put secret-dependent logic in +page.server.ts, +server.ts, or another server-only module. Send only a safe result such as { success: true, message: 'Email request accepted.' } back to the page.

Sender-address or domain problems

A valid API key does not make every from address valid. The sender must be configured for your Volanea account and should use a domain you have prepared for sending. If you change the sender, update VOLANEA_FROM_EMAIL in the environment rather than exposing sender selection in a public form.

For production, use a recognizable sender identity and make the reply path intentional. Transactional mail often benefits from a monitored reply address or a clear support route, especially for account alerts, invoices, and customer-facing notifications.

Treating API acceptance as inbox delivery

A successful HTTP response means Volanea accepted the request for processing. It is not the same thing as proof that the recipient opened the email, saw it in the inbox, or even received it from their provider. Bounces, suppression rules, recipient-server decisions, and spam filtering can occur after the initial API call.

Build product workflows around delivery events and your own application state. For example, do not mark a user as having completed an email-verification flow merely because the verification email request was accepted. Mark delivery-related states based on the appropriate signals for your workflow.

Returning provider details to the browser

Provider response bodies may be valuable in protected logs, but they do not belong in a generic user-facing error banner. Return a plain message to the form, log structured diagnostic details server-side, and attach your own request or event ID for support investigation.

This separation improves security and keeps the UX understandable: users need to know whether to retry or contact support, while engineers need status codes and sanitized provider details.

Production considerations

The sample is intentionally small, but production email code benefits from a clearer architecture. Keep provider calls in a server-only service module once multiple routes need them. A service function can accept a narrow internal message type, validate required fields, apply the same headers consistently, and centralize logging.

For example, your application might have a server-only sendTransactionalEmail function that receives to, subject, html, text, and an optional idempotency key. It should not accept a browser-provided API key or arbitrary sender address. Individual actions and webhook handlers can then call that function after completing business-specific authorization.

Consider these operational practices:

  • Store message intent in your database before or alongside the send operation.
  • Use an idempotency key for retryable, one-time events such as receipts and password-reset requests.
  • Keep HTML templates readable, accessible, and paired with useful plain text.
  • Escape or encode user-supplied values before putting them into HTML.
  • Avoid logging email contents or personal data unless your privacy and retention rules allow it.
  • Rate-limit public endpoints that can trigger email, particularly password reset and invite forms.
  • Use background processing for high-volume or non-interactive sending instead of making a user wait on a long request.
  • Monitor delivery events, bounces, complaints, and suppression outcomes.

If you collect email addresses through forms, validate their format for usability but do not assume format validation proves that an inbox exists or can receive mail. When you need an additional pre-send check for user-entered addresses, use the email address verification tool as part of your workflow. It should complement—not replace—proper consent, confirmation, and bounce handling.

Next steps: templates and webhooks

Once the direct HTML example works, move repeatable transactional content into templates. Volanea’s template API stores reusable content under a templateId, allowing a send request to reference the template instead of carrying the full markup each time. Templates make it easier to keep branding, layout, copy changes, and plain-text alternatives consistent across receipts, welcome messages, account alerts, and other system emails.

For implementation details and the current request schemas, use the email API reference and setup guides. Keep template variables server-controlled and validate the data you substitute into them. A template should make presentation consistent; it should not become a path for untrusted HTML or unreviewed recipient data.

Then add webhooks. Webhooks deliver event notifications to an endpoint you control, letting your application react to outcomes after the original send request. Typical uses include recording delivery activity, surfacing bounces to support workflows, maintaining a local notification timeline, or changing an email address’s state after a complaint or suppression event.

A webhook endpoint should be treated as an internet-facing server endpoint: verify the provider’s signature according to the current webhook documentation, reject invalid requests, process events idempotently, and respond quickly. Queue longer work after verification instead of making the webhook sender wait for database-heavy or third-party operations.

FAQ

Do I need a Volanea SDK to send email with SvelteKit?

No. This guide uses the documented REST endpoint with SvelteKit’s server-side native fetch. That avoids relying on an unverified SvelteKit-specific SDK API and is sufficient for transactional sends.

Can I call Volanea from a Svelte component?

Do not call Volanea directly from browser code because doing so would expose your secret API key. Call the API from +page.server.ts, +server.ts, or another server-only module that imports private environment variables.

Should I use sk_test_ or sk_ while developing?

Use a test key while building and testing when available. Test keys let you validate the integration without delivering live messages. Use a live secret key only in the appropriate production environment.

Why include both HTML and plain text email content?

HTML provides layout and branding, while plain text improves accessibility and gives email clients a useful alternative when HTML is unavailable or disabled. Keep both versions consistent in meaning.

Does a successful send response guarantee inbox placement?

No. It means the API accepted the send request. Actual delivery depends on later processing, sender-domain configuration, recipient-server decisions, suppression status, bounces, and filtering. Use webhooks and delivery records to observe downstream outcomes.