Developer guides

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 the SDK

npm

The 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 page
  • Contacts 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

  1. Run npm run dev, sign up with your own email, and confirm the contact appears in your Loops audience.

  2. POST to /api/password-reset with your email and check the inbox.

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

Common questions

Is the Loops API safe to call from the browser?

Why isn’t my transactional email sending?

Does updateContact create duplicate contacts?