Send emails with SvelteKit
Transactional and marketing email from a +server.ts route or form action. Your key stays in private env, out of the client build.
✓Free to start. Unlimited contacts on every tier.
One request, complete trace
Request
POST /api/v1/emails
Authorization: Bearer aem_…
Idempotency-Key: welcome-8f21
{ "to": "[email protected]", "template": "welcome" }
202 Accepted
{ "id": "msg_7ca31", "status": "queued" }
[ 01 / 05 ]Quickstart
How do I send email from SvelteKit?
Create a key, keep it on the server, and call one endpoint from a +server.ts route or form action.
Get an API key
Open Settings → API & MCP and create a key with the Send + manage permission (the default). It starts with aem_ and is shown once.
Store it as a server-side environment variable
Set AEM_KEY=aem_… in .env and read it through $env/dynamic/private. SvelteKit refuses to import $lib/server code into the browser.
src/lib/server/email.tsimport { env } from "$env/dynamic/private"; export const EMAIL_URL = "https://sendandretain.com/api/v1/emails"; export const authHeaders = (idempotencyKey: string) => ({ Authorization: `Bearer ${env.AEM_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, });Send the email
A server route posts to the email API. A retry with the same Idempotency-Key won't send a second email.
src/routes/api/welcome/+server.tsimport { json } from "@sveltejs/kit"; import { EMAIL_URL, authHeaders } from "$lib/server/email"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request }) => { const input = await request.json(); const res = await fetch(EMAIL_URL, { method: "POST", headers: authHeaders(`welcome-${input.userId}`), body: JSON.stringify({ to: input.email, template: "welcome" }) }); return json(await res.json(), { status: res.status }); };Check the response
A 202 means queued, not delivered. Keep the id, then follow delivery in the message log or through a signed webhook.
202 Accepted{ "id": "msg_7ca31", "status": "queued" }
[ 02 / 05 ]Where it goes
Where should email code live in your SvelteKit app?
Server routes and form actions
Use a form action for a page mutation, or +server.ts for an API endpoint. Both can read private env.
Events after the mutation
Send events after the server-side change succeeds, so automations follow what really happened.
A raw body for webhooks
In +server.ts, read await request.text() before parsing.
[ 03 / 05 ]Marketing email
Can I send marketing emails from SvelteKit too?
Yes. Send an event from the same form action when a user signs up, starts a trial, or goes quiet, and an automation sends the right email. Automations start paused, and every email goes to one person: no newsletter blasts.
- Transactional and marketing email share one contact record and one unsubscribe list.
- Unsubscribes, bounces, and complaints are suppressed for you before every send.
- Templates are react.email components; published versions never change under you.
[ 04 / 05 ]Example prompts
What can you ask your coding agent to do in SvelteKit?
Claude Code, Cursor, and Codex can do these over the Send & Retain MCP server, with your repo open. Automations they build stay paused until you turn them on.
Welcome email
“Add a welcome email to our SvelteKit signup action using Send & Retain, with an idempotency key.”
Order receipt
“Send an order receipt email from our checkout form action, with the API key in $env/dynamic/private.”
Webhook
“Add a SvelteKit +server.ts route that verifies Send & Retain delivery webhooks and flags bounced users.”
Trial automation
“Send a trial_started event from our SvelteKit app and build a paused trial-conversion automation for it.”
[ 05 / 05 ]Related
Keep building with SvelteKit
Questions people ask us
Send your first email from SvelteKit today.
Create a key, paste the code into a +server.ts route or form action, and watch the email land in the message log.