← Back to dashboard
resendcommsfresh

Resend Integration Guide

Text
██████╗ ███████╗███████╗███████╗███╗   ██╗██████╗
██╔══██╗██╔════╝██╔════╝██╔════╝████╗  ██║██╔══██╗
██████╔╝█████╗  ███████╗█████╗  ██╔██╗ ██║██║  ██║
██╔══██╗██╔══╝  ╚════██║██╔══╝  ██║╚██╗██║██║  ██║
██║  ██║███████╗███████║███████╗██║ ╚████║██████╔╝
╚═╝  ╚═╝╚══════╝╚══════╝╚══════╝╚═╝  ╚═══╝╚═════╝

Resend Integration Guide

Focus: Transactional email delivery for codeAmani products — send emails programmatically using React Email templates and the Resend API.

Overview

Resend is a developer-first transactional email platform. Built by the team behind React Email, it provides a clean SDK and native support for rendering React components as email HTML. Used in codeAmani products for auth notifications, payment receipts, onboarding flows, and system alerts.

Here is the core flow at a glance — your app hands off to Resend, and delivery events flow back to you via webhooks:

Official Documentation


SDK Setup

Bash
npm install resend

Initialize Client

TypeScript
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

Core Patterns

Send a Simple Email

TypeScript
// app/api/email/send/route.ts
import { Resend } from "resend";
import { NextRequest } from "next/server";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: NextRequest) {
  const { to, subject, html } = await req.json();

  const { data, error } = await resend.emails.send({
    from: "codeAmani Labs <no-reply@codeamanilabs.com>",
    to,
    subject,
    html,
  });

  if (error) {
    return Response.json({ error }, { status: 500 });
  }

  return Response.json({ id: data?.id });
}

React Email Templates

Bash
npm install react-email @react-email/components
TypeScript
// emails/WelcomeEmail.tsx
import {
  Body, Button, Container, Head, Heading,
  Html, Preview, Section, Text,
} from "@react-email/components";

interface WelcomeEmailProps {
  userName: string;
  dashboardUrl: string;
}

export function WelcomeEmail({ userName, dashboardUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to codeAmani Labs</Preview>
      <Body style={{ backgroundColor: "#f6f9fc", fontFamily: "sans-serif" }}>
        <Container style={{ margin: "0 auto", padding: "20px", maxWidth: "580px" }}>
          <Heading>Welcome, {userName}!</Heading>
          <Text>Your account is ready. Click below to get started.</Text>
          <Section style={{ textAlign: "center", margin: "32px 0" }}>
            <Button
              href={dashboardUrl}
              style={{ background: "#16a34a", color: "#fff", padding: "12px 24px", borderRadius: "6px" }}
            >
              Open Dashboard
            </Button>
          </Section>
          <Text style={{ color: "#6b7280", fontSize: "12px" }}>
            Powered by codeAmani Labs
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Send Using React Template

TypeScript
import { render } from "@react-email/render";
import { WelcomeEmail } from "@/emails/WelcomeEmail";

const html = await render(
  <WelcomeEmail
    userName={user.firstName}
    dashboardUrl={`${process.env.APP_URL}/dashboard`}
  />
);

await resend.emails.send({
  from: "codeAmani Labs <no-reply@codeamanilabs.com>",
  to: user.email,
  subject: "Welcome to codeAmani Labs",
  html,
});

Batch Send

TypeScript
await resend.batch.send([
  {
    from: "alerts@codeamanilabs.com",
    to: "user1@example.com",
    subject: "Payment Received",
    html: receipt1Html,
  },
  {
    from: "alerts@codeamanilabs.com",
    to: "user2@example.com",
    subject: "Payment Received",
    html: receipt2Html,
  },
]);

Clerk Webhook → Welcome Email Pattern

Trigger welcome emails automatically when Clerk creates a user:

TypeScript
// app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { Resend } from "resend";
import { render } from "@react-email/render";
import { WelcomeEmail } from "@/emails/WelcomeEmail";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: Request) {
  const body = await req.text();
  const svix_id = req.headers.get("svix-id")!;
  const svix_timestamp = req.headers.get("svix-timestamp")!;
  const svix_signature = req.headers.get("svix-signature")!;

  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
  const event = wh.verify(body, { "svix-id": svix_id, "svix-timestamp": svix_timestamp, "svix-signature": svix_signature }) as { type: string; data: { email_addresses: { email_address: string }[]; first_name: string } };

  if (event.type === "user.created") {
    const email = event.data.email_addresses[0].email_address;
    const html = await render(<WelcomeEmail userName={event.data.first_name} dashboardUrl={`${process.env.APP_URL}/dashboard`} />);

    await resend.emails.send({
      from: "codeAmani Labs <welcome@codeamanilabs.com>",
      to: email,
      subject: "Welcome to codeAmani Labs",
      html,
    });
  }

  return new Response("OK");
}

M-Pesa Receipt Emails

When a Daraja STK Push succeeds, the M-Pesa callback delivers the MpesaReceiptNumber, amount, and transaction date. Email a receipt as a secondary confirmation — the SMS from Safaricom is the user's primary proof, so never block the callback on the email send. Email here is a nicety (a paper trail), not the source of truth.

Here is the flow from a successful payment to a sent receipt:

PaymentReceipt Template

Amount is rendered with the KES prefix (Daraja amounts are integer KES — no decimals). The MpesaReceiptNumber is the canonical reference users quote in support.

TypeScript
// emails/PaymentReceipt.tsx
import {
  Body, Container, Head, Heading, Hr,
  Html, Preview, Row, Column, Section, Text,
} from "@react-email/components";

interface PaymentReceiptProps {
  customerName: string;
  amount: number;            // integer KES
  mpesaReceiptNumber: string;
  transactionDate: string;   // already formatted for display (EAT)
  description: string;
}

export function PaymentReceipt({
  customerName,
  amount,
  mpesaReceiptNumber,
  transactionDate,
  description,
}: PaymentReceiptProps) {
  return (
    <Html>
      <Head />
      <Preview>Payment received — KES {amount.toLocaleString("en-KE")}</Preview>
      <Body style={{ backgroundColor: "#f6f9fc", fontFamily: "sans-serif" }}>
        <Container style={{ margin: "0 auto", padding: "20px", maxWidth: "580px" }}>
          <Heading>Payment received</Heading>
          <Text>Hi {customerName}, your M-Pesa payment was successful.</Text>

          <Section style={{ background: "#fff", borderRadius: "8px", padding: "20px", marginTop: "16px" }}>
            <Row>
              <Column style={{ color: "#6b7280" }}>Amount</Column>
              <Column style={{ textAlign: "right", fontWeight: 700 }}>
                KES {amount.toLocaleString("en-KE")}
              </Column>
            </Row>
            <Hr style={{ borderColor: "#e5e7eb", margin: "12px 0" }} />
            <Row>
              <Column style={{ color: "#6b7280" }}>M-Pesa receipt</Column>
              <Column style={{ textAlign: "right" }}>{mpesaReceiptNumber}</Column>
            </Row>
            <Hr style={{ borderColor: "#e5e7eb", margin: "12px 0" }} />
            <Row>
              <Column style={{ color: "#6b7280" }}>Date</Column>
              <Column style={{ textAlign: "right" }}>{transactionDate}</Column>
            </Row>
            <Hr style={{ borderColor: "#e5e7eb", margin: "12px 0" }} />
            <Row>
              <Column style={{ color: "#6b7280" }}>For</Column>
              <Column style={{ textAlign: "right" }}>{description}</Column>
            </Row>
          </Section>

          <Text style={{ color: "#6b7280", fontSize: "12px", marginTop: "16px" }}>
            Keep this receipt for your records. Powered by codeAmani Labs.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Send on the Daraja Callback

Pass the component directly via the react property — the Resend SDK renders it to HTML for you, so no manual render() call is needed. Pass it as a function call (PaymentReceipt({ ... })), not as JSX, in a .ts route handler.

TypeScript
// app/api/mpesa/callback/route.ts
import { Resend } from "resend";
import { PaymentReceipt } from "@/emails/PaymentReceipt";
import { NextRequest } from "next/server";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: NextRequest) {
  const body = await req.json();
  const cb = body.Body.stkCallback;

  // Always ACK Daraja fast — do not block on DB or email work
  if (cb.ResultCode !== 0) {
    // Payment failed / cancelled — record and return
    return Response.json({ ResultCode: 0, ResultDesc: "Accepted" });
  }

  // Pull metadata items by Name (order is not guaranteed)
  const items: { Name: string; Value: string | number }[] =
    cb.CallbackMetadata.Item;
  const get = (name: string) => items.find((i) => i.Name === name)?.Value;

  const amount = Number(get("Amount"));
  const mpesaReceiptNumber = String(get("MpesaReceiptNumber"));
  const checkoutRequestId = cb.CheckoutRequestID;

  // Idempotency: only the FIRST processing of this CheckoutRequestID
  // should send the receipt. Daraja can retry the callback.
  const txn = await markPaidIfNew(checkoutRequestId, {
    amount,
    mpesaReceiptNumber,
  });

  if (txn.firstTime && txn.customerEmail) {
    // Fire-and-forget: never let a Resend error fail the callback ACK
    sendReceipt(txn).catch((err) => logError("receipt-email", err));
  }

  return Response.json({ ResultCode: 0, ResultDesc: "Accepted" });
}

async function sendReceipt(txn: {
  customerName: string;
  customerEmail: string;
  amount: number;
  mpesaReceiptNumber: string;
  transactionDate: string;
  description: string;
}) {
  const { error } = await resend.emails.send({
    from: "codeAmani Labs <receipts@codeamanilabs.com>",
    to: txn.customerEmail,
    subject: `Payment received — KES ${txn.amount.toLocaleString("en-KE")}`,
    react: PaymentReceipt({
      customerName: txn.customerName,
      amount: txn.amount,
      mpesaReceiptNumber: txn.mpesaReceiptNumber,
      transactionDate: txn.transactionDate,
      description: txn.description,
    }),
  });
  if (error) throw error;
}

Gotcha — idempotent send, non-blocking ACK. Daraja may deliver the same callback more than once. Gate the email behind a "first time we marked this CheckoutRequestID paid" check (markPaidIfNew returns firstTime) so a retry never double-sends a receipt. And send fire-and-forget (.catch(...)) — the route must return ResultCode 0 to Daraja promptly regardless of whether Resend is slow or down. A failed receipt email must never turn a successful payment into a failed-looking callback.


Webhooks (Delivery Events)

Resend sends delivery status events. Verify using the signing secret from your Resend dashboard:

TypeScript
// app/api/webhooks/resend/route.ts
import { Webhook } from "svix";
import { NextRequest } from "next/server";

export async function POST(req: NextRequest) {
  const body = await req.text();
  const wh = new Webhook(process.env.RESEND_WEBHOOK_SECRET!);

  const event = wh.verify(body, {
    "svix-id": req.headers.get("svix-id")!,
    "svix-timestamp": req.headers.get("svix-timestamp")!,
    "svix-signature": req.headers.get("svix-signature")!,
  }) as { type: string; data: { email_id: string; to: string[] } };

  switch (event.type) {
    case "email.delivered":
      // Mark as delivered in DB
      break;
    case "email.bounced":
      // Handle bounce — remove from mailing list
      break;
    case "email.complained":
      // Handle spam complaint
      break;
  }

  return new Response("OK");
}

Domain Setup

A quick map of getting your own domain verified — once these DNS records propagate, your custom from address works and you stay out of spam:

To send from your own domain, add DNS records via Porkbun:

Bash
# Records to add in Porkbun dashboard or via API:
# Type: TXT  Name: resend._domainkey  Value: (from Resend dashboard)
# Type: TXT  Name: @                  Value: v=spf1 include:amazonses.com ~all
# Type: MX   (if not already configured)

Preview Emails Locally

Bash
# Launch React Email preview server
npx react-email dev
# Open http://localhost:3000 to preview templates

Environment Variables

Bash
# Required
RESEND_API_KEY=re_...

# Optional
RESEND_WEBHOOK_SECRET=whsec_...

Common Use Cases

Use CaseTemplate
Welcome / onboardingWelcomeEmail.tsx triggered by user.created
M-Pesa payment receiptPaymentReceipt.tsx triggered by M-Pesa callback
Subscription confirmationSubscriptionEmail.tsx triggered by Stripe webhook
Password resetUse Clerk's built-in email — only override for custom branding
System alertsPlain HTML — no React template needed

Troubleshooting

IssueFix
Invalid API keyVerify RESEND_API_KEY starts with re_
Emails going to spamVerify domain DNS records in Resend dashboard
from domain not verifiedAdd and verify domain before using custom from
React Email not renderingRun npx react-email dev to preview template locally
Webhook 400Resend uses Svix — verify with the Svix Webhook class