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

Install and Configure

From your app directory:

bun add samva zod

Set 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 dev

Keep 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.

src/routes/api/send.ts
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.

ResultWhat to check
401 UnauthorizedSend the same route token configured on the app, with the Bearer prefix.
400 Invalid send payloadSend valid JSON with an email address, nonempty subject and text, and a stable idempotency key.
500 Email is not configuredSet both server environment variables and restart the dev server.
SDK error in the server logCheck the API key, sender setup, and SDK error handling. Do not blindly retry sends.
Accepted, but no email in the inboxInspect 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.

Related documentation

On this page