Back to Blog

How to Send Emails in SvelteKit (2026 Guide)

12 min read

SvelteKit runs server-side code in +server.ts endpoints and +page.server.ts form actions. This means you can send emails directly from your SvelteKit app without a separate backend. API keys stay on the server, and you get type-safe form handling.

This guide covers both approaches with working examples for Sequenzy, Resend, and SendGrid.

Pick a Provider

  • Sequenzy is built for SaaS. Transactional emails, marketing campaigns, automated sequences from one SDK. Native Stripe integration.
  • Resend is developer-friendly. Clean SDK. They have one-off broadcast campaigns but no automations or sequences.
  • SendGrid is the enterprise option. Good for high volume.

Install

Terminal
npm install sequenzy
Terminal
npm install resend
Terminal
npm install @sendgrid/mail

Add your API key to .env:

.env
SEQUENZY_API_KEY=sq_your_api_key_here
.env
RESEND_API_KEY=re_your_api_key_here
.env
SENDGRID_API_KEY=SG.your_api_key_here

Create a shared email client:

src/lib/server/email.ts
import Sequenzy from "sequenzy";
import { SEQUENZY_API_KEY } from "$env/static/private";

export const sequenzy = new Sequenzy({ apiKey: SEQUENZY_API_KEY });
src/lib/server/email.ts
import { Resend } from "resend";
import { RESEND_API_KEY } from "$env/static/private";

export const resend = new Resend(RESEND_API_KEY);
src/lib/server/email.ts
import sgMail from "@sendgrid/mail";
import { SENDGRID_API_KEY } from "$env/static/private";

sgMail.setApiKey(SENDGRID_API_KEY);

export { sgMail };

Note: $env/static/private is SvelteKit's way of accessing server-only env vars. These are never exposed to the client.

Send from a Server Route

src/routes/api/send-welcome/+server.ts
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { sequenzy } from "$lib/server/email";

export const POST: RequestHandler = async ({ request }) => {
const { email, name } = await request.json();

if (!email || !name) {
  return json({ error: "email and name required" }, { status: 400 });
}

const result = await sequenzy.transactional.send({
  to: email,
  subject: `Welcome, ${name}`,
  body: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
});

return json({ jobId: result.jobId });
};
src/routes/api/send-welcome/+server.ts
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { resend } from "$lib/server/email";

export const POST: RequestHandler = async ({ request }) => {
const { email, name } = await request.json();

if (!email || !name) {
  return json({ error: "email and name required" }, { status: 400 });
}

const { data, error } = await resend.emails.send({
  from: "Your App <noreply@yourdomain.com>",
  to: email,
  subject: `Welcome, ${name}`,
  html: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
});

if (error) {
  return json({ error: error.message }, { status: 500 });
}

return json({ id: data?.id });
};
src/routes/api/send-welcome/+server.ts
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { sgMail } from "$lib/server/email";

export const POST: RequestHandler = async ({ request }) => {
const { email, name } = await request.json();

if (!email || !name) {
  return json({ error: "email and name required" }, { status: 400 });
}

try {
  await sgMail.send({
    to: email,
    from: "noreply@yourdomain.com",
    subject: `Welcome, ${name}`,
    html: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
  });
  return json({ sent: true });
} catch {
  return json({ error: "Failed to send" }, { status: 500 });
}
};

Send from Form Actions

SvelteKit form actions are the idiomatic way to handle form submissions. The email sending runs server-side, so your API keys are safe.

src/routes/contact/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";
import { sequenzy } from "$lib/server/email";

export const actions = {
default: async ({ request }) => {
  const data = await request.formData();
  const email = data.get("email") as string;
  const message = data.get("message") as string;

  if (!email || !message) {
    return fail(400, { error: "All fields required", email, message });
  }

  try {
    await sequenzy.transactional.send({
      to: "you@yourcompany.com",
      subject: `Contact form: ${email}`,
      body: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
    });
    return { success: true };
  } catch {
    return fail(500, { error: "Failed to send" });
  }
},
} satisfies Actions;
src/routes/contact/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";
import { resend } from "$lib/server/email";

export const actions = {
default: async ({ request }) => {
  const data = await request.formData();
  const email = data.get("email") as string;
  const message = data.get("message") as string;

  if (!email || !message) {
    return fail(400, { error: "All fields required", email, message });
  }

  const { error } = await resend.emails.send({
    from: "Contact <noreply@yourdomain.com>",
    to: "you@yourcompany.com",
    subject: `Contact form: ${email}`,
    html: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
  });

  if (error) {
    return fail(500, { error: "Failed to send" });
  }

  return { success: true };
},
} satisfies Actions;
src/routes/contact/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";
import { sgMail } from "$lib/server/email";

export const actions = {
default: async ({ request }) => {
  const data = await request.formData();
  const email = data.get("email") as string;
  const message = data.get("message") as string;

  if (!email || !message) {
    return fail(400, { error: "All fields required", email, message });
  }

  try {
    await sgMail.send({
      to: "you@yourcompany.com",
      from: "noreply@yourdomain.com",
      subject: `Contact form: ${email}`,
      html: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
    });
    return { success: true };
  } catch {
    return fail(500, { error: "Failed to send" });
  }
},
} satisfies Actions;

The Svelte page:

<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
  import type { ActionData } from './$types';
 
  let { form }: { form: ActionData } = $props();
</script>
 
<form method="POST">
  <input name="email" type="email" placeholder="Your email" required />
  <textarea name="message" placeholder="Your message" required></textarea>
  <button type="submit">Send</button>
 
  {#if form?.error}
    <p style="color: red">{form.error}</p>
  {/if}
  {#if form?.success}
    <p style="color: green">Message sent!</p>
  {/if}
</form>

Going to Production

1. Verify Your Domain

Add SPF, DKIM, DMARC DNS records through your provider's dashboard.

2. Use SvelteKit's Private Env

Always use $env/static/private for API keys. SvelteKit prevents these from leaking to the client at build time.

3. Add Error Handling

Wrap all email sends in try/catch and return proper fail() responses.

Beyond Transactional

Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one SDK. Native Stripe integration for SaaS.

Wrapping Up

  1. Server routes (+server.ts) for API-style email endpoints
  2. Form actions (+page.server.ts) for form submissions
  3. Private env vars with $env/static/private for security
  4. Type-safe forms with SvelteKit's ActionData

Pick your provider, copy the patterns, and start sending.