Send transactional email from Next.js
Add password resets, receipts, and other operational email to a Next.js app with the Loops SDK. Server side only, working code throughout, and verified end to end by an unaided coding agent.
Who this is for
You have a Next.js app that needs to send operational email: password resets, verifications, receipts. This guide gets you from nothing to a working send in about ten minutes. And when you want marketing and lifecycle email later, you won’t need a second tool. See how transactional email works on Loops.
Give this to your agent
Using Claude Code, Cursor, or Codex? Paste this prompt and it can do everything below for you.
Install Loops (loops.so) in this Next.js app: npm install loops, create
lib/loops.ts exporting a server-only LoopsClient reading LOOPS_API_KEY,
upsert a contact on signup with updateContact, and add a password-reset
endpoint using sendTransactionalEmail with a transactionalId from the
Loops dashboard. Docs: https://loops.so/docs/sdks/javascript and
https://loops.so/docs/transactional/guideInstall the SDK
npm install loopsThe package is loops. Not loops-js (that’s the GitHub repo name), not @loops/sdk, and not the unrelated crypto packages that show up in search results. Then create a small server-only client:
// lib/loops.ts (server side only)
import { LoopsClient } from "loops";
let client: LoopsClient | null = null;
export function getLoops(): LoopsClient {
if (!process.env.LOOPS_API_KEY) {
throw new Error("LOOPS_API_KEY is not set");
}
client ??= new LoopsClient(process.env.LOOPS_API_KEY);
return client;
}Sync the contact on signup
After creating the user in your database, upsert the contact in Loops. updateContact creates the contact if it doesn’t exist yet, so calling it on every signup is safe. Wrap it in try/catch if signup should succeed even when the sync fails.
// pages/api/signup.ts (same shape in an App Router route handler)
import { getLoops } from "../../lib/loops";
await getLoops().updateContact({
email,
properties: { source: "signup", signedUpAt: new Date().toISOString() },
});Send the password reset
First create a transactional email in the Loops dashboard with a {resetUrl} data variable and publish it. The transactionalId is on the Publish page under API details.
// pages/api/password-reset.ts
import { getLoops } from "../../lib/loops";
await getLoops().sendTransactionalEmail({
transactionalId: process.env.LOOPS_PASSWORD_RESET_TRANSACTIONAL_ID!,
email,
dataVariables: { resetUrl },
});Return the same generic 200 whether or not the account exists, so the endpoint can’t be used to discover which emails have accounts.
Environment
# .env.local (server side only, no NEXT_PUBLIC_ prefix)
LOOPS_API_KEY= # Settings -> API
LOOPS_PASSWORD_RESET_TRANSACTIONAL_ID= # the template's Publish pageContacts are keyed by email, or by userId if you prefer.
properties accepts string, number, boolean, and null values, and you can create custom properties freely.
dataVariables keys are case sensitive: letters, numbers, underscores, and dashes.
Test it
Run npm run dev, sign up with your own email, and confirm the contact appears in your Loops audience.
POST to /api/password-reset with your email and check the inbox.
Run npx tsc --noEmit and make sure it passes.
Common failure modes
Wrong package installed. It’s loops. Unrelated packages like @loops-fi/sdk and @loop-crypto/loop-sdk appear in searches; they are crypto projects, not Loops.
Calling from the client. The API key must never reach browser code. Keep every call in API routes or server components.
Unpublished template. sendTransactionalEmail needs a published template; drafts don’t send.
Data variable case mismatch. {resetUrl} and {resetURL} are different variables.
Rate limits. The API allows 10 requests per second per team. The SDK throws RateLimitExceededError, so catch it if you send in bursts.
Keep going
Trigger onboarding sequences and trial nudges from product events with sendEvent, or hand the whole setup to your coding agent on the Loops agents page. For the bigger picture, see transactional email on Loops.
Related guides: send email from AI agents and migrate from SendGrid to Loops.
Common questions
Is the Loops API safe to call from the browser?
Why isn’t my transactional email sending?
Does updateContact create duplicate contacts?