Developer guides

Send transactional email from Node.js

Add password resets, receipts, invites, and alerts to any Node.js backend with the official Loops SDK. Working code for Express, Fastify, queue workers, cron jobs, and plain scripts.

Who this is for

You have a Node.js backend and need to send operational email. This guide gets you from npm install to a delivered message, without tying the code to a framework. If you add marketing or lifecycle email later, the same Loops account and contact record can handle that too.

Give this to your agent

Using Claude Code, Cursor, or Codex? Paste this prompt into the repository. It covers the setup, first send, retry policy, and duplicate protection.



Install the SDK

npm

The package is loops. Version 7 supports TypeScript, ESM, and CommonJS, uses native fetch, and requires Node 18 or later. Keep the client in server code because the API key grants account access.

// lib/loops.ts
import { LoopsClient } from "loops";

if (!process.env.LOOPS_API_KEY) {
  throw new Error("LOOPS_API_KEY is not set");
}

export const loops = new LoopsClient(process.env.LOOPS_API_KEY);

Set up the API key

Generate a key under Settings → API in Loops. Store it in your deployment environment and never expose it to browser code, mobile apps, logs, or a committed file.

# .env
LOOPS_API_KEY=
LOOPS_PASSWORD_RESET_TRANSACTIONAL_ID

Send your first email

Create a transactional email in Loops, add a {resetUrl} data variable, and publish it. Copy the transactional ID from the API details on the Publish page.

import { loops } from "./lib/loops";

export async function sendPasswordReset(email: string, resetUrl: string) {
  return loops.sendTransactionalEmail({
    transactionalId: process.env.LOOPS_PASSWORD_RESET_TRANSACTIONAL_ID!,
    email,
    dataVariables: { resetUrl },
  });
}

This calls POST /api/v1/transactional. Every required data variable must be present and its name is case-sensitive. The recipient does not need to be in your Audience. Add addToAudience: true only when you also want to create a marketing contact.

Retry safely with idempotency

import { APIError, RateLimitExceededError } from "loops";
import { loops } from "./lib/loops";

type SendInput = Parameters<typeof loops.sendTransactionalEmail>[0];

export async function sendWithRetry(
  input: SendInput,
  idempotencyKey: string,
  attempts = 3,
) {
  for (let attempt = 0; attempt < attempts; attempt++) {
    try {
      return await loops.sendTransactionalEmail({
        ...input,
        headers: { "Idempotency-Key": idempotencyKey },
      });
    } catch (error) {
      if (error instanceof APIError && error.statusCode === 409) {
        return { success: true };
      }

      const retryable =
        error instanceof RateLimitExceededError ||
        (error instanceof APIError && error.statusCode >= 500);

      if (!retryable || attempt === attempts - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 500 * 2 ** attempt));
    }
  }
}

Loops allows 10 API requests per second per team. Retry only 429 and 5xx responses. Generate one idempotency key for the business event, persist it, and reuse it for every attempt. A repeated key returns 409 for 24 hours instead of sending the email twice.

Test without sending real email

Send to [email protected] or [email protected] first. Loops returns a normal success response but does not deliver to those reserved domains. Then send to your own inbox, confirm the record under Transactional → Metrics, remove one required data variable to verify the 4xx path, and run npx tsc --noEmit.

Common failure modes

Wrong package: install loops, not loops-js or a similarly named crypto package. Client-side calls: keep the API key on the server. Unpublished template: drafts do not send. Missing data: required variables are case-sensitive. Duplicate email: reuse one Idempotency-Key across retries. Burst traffic: catch RateLimitExceededError and back off.

Keep transactional email transactional

Password resets, receipts, verifications, and security alerts can send regardless of subscription status, so keep them free of promotion. Read transactional email on Loops for the category boundary, or use product and lifecycle email for onboarding, trial, and win-back messages.

Common questions

Can I send transactional email from any Node.js framework?

How do I prevent duplicate transactional emails?

Which API errors should I retry?