Resend Integration Guide
██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗
██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗
██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║
██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║
██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝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
| Resource | URL |
|---|---|
| Resend Docs | https://resend.com/docs |
| Node.js SDK | https://github.com/resend/resend-node |
| React Email | https://react.email/docs |
| API Reference | https://resend.com/docs/api-reference |
| Webhooks | https://resend.com/docs/dashboard/webhooks/introduction |
SDK Setup
npm install resendInitialize Client
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);Core Patterns
Send a Simple Email
// 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
npm install react-email @react-email/components// 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
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
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:
// 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.
// 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.
// 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
CheckoutRequestIDpaid" check (markPaidIfNewreturnsfirstTime) so a retry never double-sends a receipt. And send fire-and-forget (.catch(...)) — the route must returnResultCode 0to 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:
// 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:
# 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
# Launch React Email preview server
npx react-email dev
# Open http://localhost:3000 to preview templatesEnvironment Variables
# Required
RESEND_API_KEY=re_...
# Optional
RESEND_WEBHOOK_SECRET=whsec_...Common Use Cases
| Use Case | Template |
|---|---|
| Welcome / onboarding | WelcomeEmail.tsx triggered by user.created |
| M-Pesa payment receipt | PaymentReceipt.tsx triggered by M-Pesa callback |
| Subscription confirmation | SubscriptionEmail.tsx triggered by Stripe webhook |
| Password reset | Use Clerk's built-in email — only override for custom branding |
| System alerts | Plain HTML — no React template needed |
Troubleshooting
| Issue | Fix |
|---|---|
Invalid API key | Verify RESEND_API_KEY starts with re_ |
| Emails going to spam | Verify domain DNS records in Resend dashboard |
from domain not verified | Add and verify domain before using custom from |
| React Email not rendering | Run npx react-email dev to preview template locally |
| Webhook 400 | Resend uses Svix — verify with the Svix Webhook class |