For the complete documentation index, see llms.txt. This page is also available as Markdown.

Better Auth

Learn how to send Better Auth verification and password reset emails using Mailtrap's Email API.

Overview

This guide explains how to integrate Mailtrap with Better Auth to deliver the emails your authentication flows depend on, such as email verification links and password reset tokens.

Better Auth does not send email itself. It calls a function you provide, so you decide which provider delivers the message. This guide implements that function with the Mailtrap Email API.

Better Auth sends email from several places. Core email and password authentication uses two callbacks, and some plugins add their own:

Surface
Callback

Email verification

emailVerification.sendVerificationEmail

Password reset

emailAndPassword.sendResetPassword

Magic Link plugin

magicLink({ sendMagicLink })

Email OTP plugin

emailOTP({ sendVerificationOTP })

Organization plugin

organization({ sendInvitationEmail })

All of them are plain async functions, so a single sendEmail helper can serve every one.

Prerequisites

Before you start, make sure you have:

Installation

Install the Mailtrap Node.js SDK:

Send emails using Better Auth and Mailtrap

First, create a reusable sendEmail function backed by the Mailtrap Email API:

Then pass it to Better Auth to handle email verification and password resets:

Plugin emails

If you use the Magic Link, Email OTP, or Organization plugins, reuse the same sendEmail helper for their callbacks. Add the plugins array to the same betterAuth call shown above, keeping only the plugins your app actually uses:

Configuration

Once you copy the scripts, update the following:

  • Set MAILTRAP_API_KEY in your environment to your Mailtrap API token

  • Replace no-reply@yourdomain.com with an address on your verified sending domain

  • Replace Your App with the sender name you want recipients to see

  • Replace https://yourdomain.com in the invitation link with your app's base URL

The category field is optional. Setting it per email type lets you filter authentication emails in Email Logs and see the category on delivery webhooks, so you can tell reset emails from verification emails at a glance.

The link-based examples send both html and text. The HTML version gives recipients a clickable link, and the plain text version is a fallback for clients that do not render HTML. The one-time code is plain text only, since there is nothing to link to.

Escape every user-controlled value you interpolate into an html body. The invitation example runs the organization name through escapeHtml because whoever created the organization chose that name: left raw, a name containing </a><a href="..."> closes your link and opens theirs, so the email arrives from your verified domain carrying an attacker's URL. That makes it a convincing phishing message, and invitations are the worst case because Better Auth sends them to people who are not users yet and have no way to judge what is normal for your app.

Watch for this anywhere the value is not authored by you, including display names, team and organization names, and any custom note attached to an invitation. Escaping is only needed for html; text and subject are not parsed as markup, which is why the examples leave them as they are.

For richer, designed emails, keep the markup out of your application code and use Email Templates instead. Templates need a different payload, so add a separate helper rather than reusing sendEmail:

Mailtrap renders the template, so the variables are passed as data and do not need HTML escaping.

Better Auth recommends not awaiting the email call inside these handlers, which is why the examples use void. Awaiting it makes the response time depend on whether the address exists, which can leak that information. On serverless platforms, use waitUntil or the equivalent so the request is not torn down before the email is sent.

Because nothing awaits the result, sendEmail has to handle its own failures. That is what the try/catch is for: without it, a rejected send becomes an unhandled promise rejection, which terminates the Node.js process by default. Replace console.error with your own logger or error reporting.

Learn more

For additional details about the Email API, refer to the Mailtrap Email Sending API Integration guide and the Mailtrap Node.js SDK.

Last updated

Was this helpful?