Samva is in early access — self-serve signup is limited. Have a team invite? Sign up with that email. Contact us for access.

Clerk

Send Clerk auth and lifecycle email with Samva.

Clerk

Use Clerk webhooks to send auth and lifecycle email through Samva. Clerk verifies users and signs webhook events; your route verifies the Svix signature with Clerk's helper, then sends through the samva SDK.

Clerk owns authentication. Samva sends from the verified sender configured on your account, so there is no from field in the send payload.

Install

bun add @clerk/nextjs@^7.5.10 samva server-only

Add React Email when you want component templates:

bun add @react-email/render @react-email/components react react-dom

Use a Clerk version that resolves @clerk/backend >=2.4.0. Clerk fixed an improper webhook-signature acceptance issue in that line. @clerk/nextjs@6.23.3 and newer include the patched v2 backend dependency, and @clerk/nextjs@^7.5.10 already resolves to the patched v3 backend line.

Create a server-only Samva client

// lib/samva.ts
import "server-only";

import { createClient } from "samva";

const apiKey = process.env.SAMVA_API_KEY;
if (!apiKey) {
  throw new Error("SAMVA_API_KEY is not set.");
}

export const samva = createClient({ apiKey });

Configure both secrets only on the server:

SAMVA_API_KEY=sk_sm_...
CLERK_WEBHOOK_SIGNING_SECRET=whsec_...

Keep the webhook route public

Clerk webhook requests are signed, but they are not signed-in user requests. Make the webhook path public in clerkMiddleware().

// proxy.ts in Next.js 16+. Use middleware.ts in Next.js 15 and earlier.
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isPublicRoute = createRouteMatcher(["/", "/api/webhooks/clerk"]);

export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect();
  }
});

Send a welcome email on user.created

// app/api/webhooks/clerk/route.ts
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { render, toPlainText } from "@react-email/render";
import type { NextRequest } from "next/server";

import WelcomeEmail from "../../../../emails/welcome";
import { samva } from "../../../../lib/samva";

export const runtime = "edge";

export async function POST(request: NextRequest) {
  let event: Awaited<ReturnType<typeof verifyWebhook>>;

  try {
    event = await verifyWebhook(request);
  } catch {
    return new Response("Verification failed", { status: 400 });
  }

  if (event.type === "user.created") {
    const email =
      event.data.email_addresses.find(
        (address) => address.id === event.data.primary_email_address_id,
      )?.email_address ?? event.data.email_addresses[0]?.email_address;

    if (email) {
      try {
        const html = await render(
          WelcomeEmail(event.data.first_name ? { firstName: event.data.first_name } : {}),
        );

        await samva.messages.send({
          to: [{ email }],
          channel: "email",
          email: {
            subject: "Welcome",
            html,
            text: toPlainText(html),
          },
        });
      } catch (error) {
        console.error("Failed to send Clerk welcome email", error);
        return new Response("OK", { status: 200 });
      }
    }
  }

  return new Response("OK", { status: 200 });
}

verifyWebhook() reads the raw request body and checks the Svix headers. Do not parse JSON before verification. Return 400 for missing or invalid signatures.

Custom delivery with email.created

For Clerk-rendered auth email, open a Clerk email template and turn off Delivered by Clerk. Clerk emits email.created; forward the rendered email through Samva:

if (event.type === "email.created") {
  const to = event.data.to_email_address;

  if (to) {
    try {
      await samva.messages.send({
        to: [{ email: to }],
        channel: "email",
        email: {
          subject: event.data.subject ?? "Clerk email",
          html: event.data.body ?? undefined,
          text: event.data.body_plain ?? undefined,
        },
      });
    } catch (error) {
      console.error("Failed to send Clerk auth email", error);
    }
  }
}

You can also render your own React Email template from event.data.data. otp_code is documented for verification email; other keys vary by template slug, so inspect a first live event before relying on them.

Runtime notes

verifyWebhook() accepts a standard web Request, and the Samva SDK is fetch-based. The same pattern works in Next.js Route Handlers, Vercel Edge Functions, Cloudflare Workers, and Hono routes that expose a web request.

Use the svix-id header as your idempotency key if duplicate sends matter. Svix retries non-2xx responses.

Cookbook and example

The cookbook and example links resolve after the companion samva-integrations PR lands.

On this page