Send Email with TanStack Start
Add an authenticated email endpoint to TanStack Start, keep credentials on the server, and check your first send.
Send transactional email from an existing TanStack Start app with the samva SDK.
This guide adds a POST /api/send server route that accepts requests from trusted
server-side callers. It uses a Node.js runtime and plain-text email so you can
verify the integration before adding templates.
For a React form calling your own backend, use a server function instead. The
server-function cookbook
covers createServerFn and useServerFn. Enforce session authentication and
recipient authorization inside the mutation; a page's beforeLoad does not
protect the send endpoint.
Before You Start
- Have a running TanStack Start app.
- Create an API key in Developers → API keys in your Samva dashboard.
- Verify your sending domain and choose an address on that domain.
- Choose a recipient inbox you control for the first send.
Install and Configure
From your app directory:
bun add samva zodSet these environment variables in the shell that starts your development server. Generate a separate random token to protect your app's endpoint:
export SAMVA_API_KEY="samva_sk_live_your_api_key"
export SAMVA_SEND_TOKEN="$(openssl rand -hex 32)"
bun run devKeep both values server-only. Do not prefix them with VITE_, commit them, or put
the route token in browser code. Configure the same variables in your deployment's
secret settings. This example reads Node.js process.env; other runtimes need
their own environment bindings.
Add the Send Endpoint
Create this route file. TanStack Start generates its route registration when the dev server runs. Requests must carry the route token before any send is attempted.
import { createFileRoute } from "@tanstack/react-router";
import { createClient } from "samva";
import { z } from "zod";
const sendInput = z.object({
to: z.string().email(),
from: z.string().email().optional(),
subject: z.string().min(1).max(200),
text: z.string().min(1).max(10_000),
idempotencyKey: z.string().min(1).max(255),
});
export const Route = createFileRoute("/api/send")({
server: {
handlers: {
POST: async ({ request }) => {
const apiKey = process.env.SAMVA_API_KEY!;
const sendToken = process.env.SAMVA_SEND_TOKEN;
if (!apiKey || !sendToken) {
return Response.json({ error: "Email is not configured" }, { status: 500 });
}
if (request.headers.get("authorization") !== `Bearer ${sendToken}`) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const parsed = sendInput.safeParse(await request.json().catch(() => null));
if (!parsed.success) {
return Response.json({ error: "Invalid send payload" }, { status: 400 });
}
const samva = createClient({ apiKey });
const { to, from, subject, text, idempotencyKey } = parsed.data;
const message = await samva.email.send(
{
to,
...(from ? { from: { email: from } } : {}),
subject,
text,
},
{ idempotencyKey },
);
return Response.json({ id: message.id }, { status: 202 });
},
},
},
});The SDK passes idempotencyKey as Samva's send header and throws on an API
error. The handler only returns 202 after Samva accepts the request. Omit
from to use your default sender, or supply a verified sender address; see
Send an email.
The trusted caller must assign one key per intended email and retain it for retries. Reuse the same key with the same payload if a response is lost. A new key creates a new message; reusing a key with different content returns a conflict. Do not generate a fresh key inside this handler on every request.
The token grants permission to choose the recipient and content. Share it only with trusted server-side callers. For customer-facing forms, authenticate the session, derive allowed recipients on the server, and apply rate limits before calling Samva.
Send and Check the Result
In another terminal, set SAMVA_SEND_TOKEN to the same value used by your app.
Replace you@example.com with your test inbox, and use the origin printed by your
dev server if it differs from http://localhost:3000. Set operation_key once;
reuse it and the same payload if you retry this send.
operation_key="$(openssl rand -hex 16)"
curl --fail-with-body http://localhost:3000/api/send \
-H "Authorization: Bearer $SAMVA_SEND_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"to\":\"you@example.com\",\"subject\":\"Hello from TanStack Start\",\"text\":\"Your email integration works.\",\"idempotencyKey\":\"$operation_key\"}"The response contains a message ID, for example {"id":"msg_7q2xk9mvt4znw8rh"}.
Acceptance is not delivery: check the message in the dashboard and your recipient
inbox. To track delivery in your application, use
signed webhooks or retrieve the message with
samva.messages.get.
| Result | What to check |
|---|---|
401 Unauthorized | Send the same route token configured on the app, with the Bearer prefix. |
400 Invalid send payload | Send valid JSON with an email address, nonempty subject and text, and a stable idempotency key. |
500 Email is not configured | Set both server environment variables and restart the dev server. |
| SDK error in the server log | Check the API key, sender setup, and SDK error handling. Do not blindly retry sends. |
| Accepted, but no email in the inbox | Inspect delivery events, bounces, and deliverability troubleshooting. |
Runnable Project and Next Steps
The TanStack Start example
contains a complete app, environment template, form, and authenticated server route.
Follow its setup and run instructions.
Its route returns { "ok": true }; the route above returns the message ID for
tracking the first send.
- Server functions and form example: call the send path from React.
- React Email integration: render component templates when you need HTML.
- Better Auth integration: send verification and password-reset emails from auth callbacks.
- TanStack Start server routes: request handlers and middleware.
- Choose another framework: Next.js, Hono, Astro, and SvelteKit.