How to Send Emails in SvelteKit (2026 Guide)

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
npm install sequenzynpm install resendnpm install @sendgrid/mailAdd your API key to .env:
SEQUENZY_API_KEY=sq_your_api_key_hereRESEND_API_KEY=re_your_api_key_hereSENDGRID_API_KEY=SG.your_api_key_hereCreate a shared email client:
import Sequenzy from "sequenzy";
import { SEQUENZY_API_KEY } from "$env/static/private";
export const sequenzy = new Sequenzy({ apiKey: SEQUENZY_API_KEY });import { Resend } from "resend";
import { RESEND_API_KEY } from "$env/static/private";
export const resend = new Resend(RESEND_API_KEY);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
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 });
};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 });
};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.
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;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;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
- Server routes (
+server.ts) for API-style email endpoints - Form actions (
+page.server.ts) for form submissions - Private env vars with
$env/static/privatefor security - Type-safe forms with SvelteKit's
ActionData
Pick your provider, copy the patterns, and start sending.