Skip to main content
This guide shows how to use Loops to send Better Auth authentication emails for the defatult “Email & Password” authentication method. For this guide, you will need a working app with Better Auth installed and a Loops account with a verified sending domain set up.

Create transactional emails in Loops

The first step is to create transactional templates in Loops for each auth message. In this guide we have three example emails for the Email & Password, Magic Link and Email OTP authentication methods. For the authentication method you are using, you should create transactional emails for these emails in Loops. Include data variables for the data that will be passed from Better Auth. You can add data variables by typing ”{”, via the {} button above the editor, or by using the / slash menu. Adding data variables to a transactional email After publishing each transactional email, note the Transactional ID from the Review page. You will need this for the next step. Copying the transactional ID
The following variable names are suggestions and can be changed to whatever you want. Just make sure to use the same names in your Better Auth config code and in your Loops emails.

Email & Password data variables

Depending on the authentication method you are using, you will need to add different data variables to your emails.

Verification email

This email is sent when a user is requested to verify their email address.
  • verificationUrl
  • token

Password reset email

This email is sent when a user requests to reset their password.
  • resetPasswordUrl
  • token
This email is sent when a user signs in with a magic link.
  • magicLinkUrl
  • token

Email OTP data variables

Sign in email

This email is sent when a user signs in using an email OTP.
  • otpCode

Email verification email

This email is sent when a user needs to verify their email using an email OTP.
  • otpCode

Password reset email

This email is sent when a user resets their password using an email OTP.
  • otpCode

Add the Loops SDK

Install the Loops SDK in your project. This will allow you to send transactional emails via Loops.
npm i loops
Next you need to add some environment variables to your project, for example in an .env file. You can create an API key from your API Settings page. The transactional IDs are the IDs of the transactional emails you created in the previous step. (These are not all required, you will only need the ones for the authentication method you are using.)
LOOPS_API_KEY=replace-with-your-loops-api-key

# Email & Password
LOOPS_VERIFY_EMAIL_TRANSACTIONAL_ID=replace-with-verify-template-id
LOOPS_RESET_PASSWORD_TRANSACTIONAL_ID=replace-with-reset-template-id

# Magic Link
LOOPS_MAGIC_LINK_TRANSACTIONAL_ID=replace-with-magic-link-template-id

# Email OTP
LOOPS_OTP_SIGNIN_TRANSACTIONAL_ID=replace-with-email-otp-signin-template-id
LOOPS_OTP_VERIFICATION_TRANSACTIONAL_ID=replace-with-email-otp-verification-template-id
LOOPS_OTP_PASSWORD_RESET_TRANSACTIONAL_ID=replace-with-email-otp-password-reset-template-id

Create a Loops server helper

Next, create a helper function to keep email sending logic clean and reusable. This helper will be used in the next step to configure Better Auth to send emails through Loops.
It is important that this helper is used server-side only. Never expose your Loops API key in client-side code.
lib/loops.ts
import { LoopsClient } from "loops";

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

type DataVariables = Record<string, string | number | boolean | null>;

export async function sendLoopsTransactionalEmail(input: {
  email: string;
  dataVariables?: DataVariables;
}) {
  const { transactionalId, email, dataVariables } = input;
  transactionalId: string;

  const response = await loops.sendTransactionalEmail({
    transactionalId,
    email,
    dataVariables,
  });

  if (!response.success) {
    throw new Error(response.message || "Failed to send Loops transactional email");
  }
}

Configure Better Auth to send through Loops

In your Better Auth config, enable the authentication method you want to use and add callbacks to send emails. Inside the sendLoopsTransactionalEmail() calls, we populate the content for the data variables you added in your Loops emails.
Make sure that the data variable names inside dataVariables match the names of the data variables you added in your Loops emails.
Better Auth recommends not awaiting email sends in these callbacks to reduce timing-attack risk and keep auth responses fast, so we use void. If your platform supports background work (for example waitUntil), you can use that instead.

Password reset

For this email, make sure that emailAndPassword.enabled is set to true, then add a sendResetPassword callback containing our sendLoopsTransactionalEmail() helper function. Better Auth docs
auth.ts
import { betterAuth } from "better-auth";
import { sendLoopsTransactionalEmail } from "./lib/loops";

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    sendResetPassword: async ({ user, url, token }) => {
      void sendLoopsTransactionalEmail({
        transactionalId: process.env.LOOPS_RESET_PASSWORD_TRANSACTIONAL_ID as string,
        email: user.email,
        dataVariables: {
          resetPasswordUrl: url,
          token,
        },
      });
    },
  },
});

Email verification

For this email, add a sendVerificationEmail callback containing our sendLoopsTransactionalEmail() helper function. Better Auth docs
auth.ts
import { betterAuth } from "better-auth";
import { sendLoopsTransactionalEmail } from "./lib/loops";

export const auth = betterAuth({
  emailVerification: {
    sendVerificationEmail: async ({ user, url, token }) => {
      void sendLoopsTransactionalEmail({
        transactionalId: process.env.LOOPS_VERIFY_EMAIL_TRANSACTIONAL_ID as string,
        email: user.email,
        dataVariables: {
          verificationUrl: url,
          token,
        },
      });
    },
  },
});
Add the magicLink plugin to your Better Auth config and add a sendMagicLink callback containing our sendLoopsTransactionalEmail() helper function. Better Auth docs
auth.ts
import { betterAuth } from "better-auth";
import { magicLink } from "better-auth/plugins"; 
import { sendLoopsTransactionalEmail } from "./lib/loops";

export const auth = betterAuth({
  plugins: [
    magicLink: {
      sendMagicLink: async ({ email, token, url }) => {
        void sendLoopsTransactionalEmail({
          transactionalId: process.env.LOOPS_MAGIC_LINK_TRANSACTIONAL_ID as string,
          email,
          dataVariables: {
            magicLinkUrl: url,
            token,
          },
        });
      },
    },
  ],
});

Email OTP plugin

Add the emailOTP plugin to your Better Auth config and add a sendVerificationOTP callback containing our sendLoopsTransactionalEmail() helper function. There are three types of OTP emails: sign in, email verification and password reset, so you will need to create an email template for each type in Loops. Better Auth docs
auth.ts
import { betterAuth } from "better-auth";
import { emailOTP } from "better-auth/plugins"; 
import { sendLoopsTransactionalEmail } from "./lib/loops";

export const auth = betterAuth({
  plugins: [
    emailOTP: {
      async sendVerificationOTP({ email, otp, type }) { 
        if (type === "sign-in") { 
          void sendLoopsTransactionalEmail({
            transactionalId: process.env.LOOPS_OTP_SIGNIN_TRANSACTIONAL_ID as string,
            email,
            dataVariables: {
              otpCode: otp,
            },
          });
        } else if (type === "email-verification") { 
          void sendLoopsTransactionalEmail({
            transactionalId: process.env.LOOPS_OTP_VERIFICATION_TRANSACTIONAL_ID as string,
            email,
            dataVariables: {
              otpCode: otp,
            },
          });
        } else { 
          void sendLoopsTransactionalEmail({
            transactionalId: process.env.LOOPS_OTP_PASSWORD_RESET_TRANSACTIONAL_ID as string,
            email,
            dataVariables: {
              otpCode: otp,
            },
          });
        } 
      }, 
    },
  ],
});

Call Better Auth from your app

Now your app is set up to send Better Auth emails through Loops. For example, Loops will send a transactional when you sign in a user with an email OTP:
const data = await auth.api.sendVerificationOTP({
  body: {
    email: "user@example.com", // required
    type: "sign-in", // required
  },
});
Or when you reset a user’s password:
const data = await auth.api.requestPasswordReset({
  body: {
    email: "john.doe@example.com", // required
    redirectTo: "https://example.com/reset-password",
  },
});

Troubleshooting

If your emails are not sending:
  • Confirm each transactional email in Loops is Published
  • Confirm each data variable name in your Better Auth code matches the name of the data variable in your Loops email
  • Check that you’re using a valid Loops API, from the correct account
  • Check that your sending domain is verified in Loops

Read more

Send transactional email

Loops API endpoint used to send transactional emails.

Send emails from Bolt.new

Read our guide for sending emails from Bolt.new.

Integrations

Integrate Loops into lots more platforms.
Last modified on April 14, 2026