Email OTP not working when sendVerificationEmail is existing #2494

Closed
opened 2026-03-13 09:59:09 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @GautheyValentin on GitHub (Dec 9, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Register and change email

Current vs. Expected behavior

When sendVerificationEmail is in emailVerification

Current behavior on register

Sending verification email

Expected behavior on register

Send OTP code

When sendVerificationEmail is not in emailVerification

Current behavior on register

Normal

Current behavior on change email

Sending a confirmation email to the current email

Expecting behavior on change email

Send a confirmation email to the current email
Send a OTP email to the new email (by preference but can be link too)

What version of Better Auth are you using?

1.4.6

System info

{
  "node": {
    "version": "v23.6.1",
    "env": "development"
  },
  "databases": [
    {
      "name": "pg",
      "version": "8.16.3"
    },
    {
      "name": "kysely",
      "version": "0.28.8"
    }
  ],
  "betterAuth": {
    "version": "1.4.6",
    "config": null
  }
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

betterAuth({
      //...
      user: {
        modelName: 'users',
        changeEmail: {
          enabled: true,
          sendChangeEmailConfirmation: async ({ user, url, token }) => {
            // SEND CHANGE EMAIL CONFIRMATION
          },
        },
      emailVerification: {
        sendOnSignUp: true,
        sendOnSignIn: true,
        autoSignInAfterVerification: true,
        expiresIn: 60 * 5, // 5 minutes
        sendVerificationEmail: async ({ user, url, token }) => {
          // SEND EMAIL
        },
      },
      //...code
      plugins: [
        bearer({ requireSignature: true }),
        username({
          minUsernameLength: 3,
          maxUsernameLength: 30,
        }),
        emailOTP({
          otpLength: 6,
          allowedAttempts: 5,
          expiresIn: 60 * 5, // 5 minutes
          sendVerificationOTP: async ({ email, otp, type }, ctx) => {
            // SEND EMAIL WITH OTP
          },
          overrideDefaultEmailVerification: true,
        }),
      ],
     
    });

Additional context

Since 1.4. Before introduce sendChangeEmailConfirmation

Originally created by @GautheyValentin on GitHub (Dec 9, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Register and change email ### Current vs. Expected behavior ## When sendVerificationEmail *is in* emailVerification ### Current behavior on register Sending verification email ### Expected behavior on register Send OTP code ## When sendVerificationEmail *is not in* emailVerification ### Current behavior on register Normal ### Current behavior on change email Sending a confirmation email to the current email ### Expecting behavior on change email Send a confirmation email to the current email Send a OTP email to the new email (by preference but can be link too) ### What version of Better Auth are you using? 1.4.6 ### System info ```bash { "node": { "version": "v23.6.1", "env": "development" }, "databases": [ { "name": "pg", "version": "8.16.3" }, { "name": "kysely", "version": "0.28.8" } ], "betterAuth": { "version": "1.4.6", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript betterAuth({ //... user: { modelName: 'users', changeEmail: { enabled: true, sendChangeEmailConfirmation: async ({ user, url, token }) => { // SEND CHANGE EMAIL CONFIRMATION }, }, emailVerification: { sendOnSignUp: true, sendOnSignIn: true, autoSignInAfterVerification: true, expiresIn: 60 * 5, // 5 minutes sendVerificationEmail: async ({ user, url, token }) => { // SEND EMAIL }, }, //...code plugins: [ bearer({ requireSignature: true }), username({ minUsernameLength: 3, maxUsernameLength: 30, }), emailOTP({ otpLength: 6, allowedAttempts: 5, expiresIn: 60 * 5, // 5 minutes sendVerificationOTP: async ({ email, otp, type }, ctx) => { // SEND EMAIL WITH OTP }, overrideDefaultEmailVerification: true, }), ], }); ``` ### Additional context Since 1.4. Before introduce sendChangeEmailConfirmation
Author
Owner

@dosubot[bot] commented on GitHub (Dec 9, 2025):

Hi @GautheyValentin! I'm Dosu and I’m helping the better-auth team.

The reason your OTP emails aren't sent when sendVerificationEmail is present is because Better Auth's config merging gives priority to your explicit sendVerificationEmail function over the emailOTP plugin's overrideDefaultEmailVerification option. So, during registration and email change, the system always calls your sendVerificationEmail and sends a verification link, not an OTP code. The override only works if sendVerificationEmail is not defined in your config—then the plugin can replace the default behavior and send OTPs instead. source

For registration, if you want only OTP emails, remove sendVerificationEmail from your emailVerification config and rely on the emailOTP plugin with overrideDefaultEmailVerification: true. source

For change email, Better Auth doesn't natively support sending OTPs to the new email—the default flow sends a link. If you want to send an OTP for email change, you'll need a custom implementation: generate/store an OTP for the new email, send it in your own handler, and create an endpoint to verify the OTP before updating the user's email. source

If you want to send both a verification link and OTP in the same email, you can manually generate an OTP inside your sendVerificationEmail function and include both in the email content, but this requires handling OTP generation and storage yourself. There's an open PR to support this natively, but it's not available in 1.4.6 yet. source

Let me know if you want example code for any of these workarounds.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

@dosubot[bot] commented on GitHub (Dec 9, 2025): <!-- Greeting --> Hi @GautheyValentin! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The reason your OTP emails aren't sent when sendVerificationEmail is present is because Better Auth's config merging gives priority to your explicit sendVerificationEmail function over the emailOTP plugin's overrideDefaultEmailVerification option. So, during registration and email change, the system always calls your sendVerificationEmail and sends a verification link, not an OTP code. The override only works if sendVerificationEmail is not defined in your config—then the plugin can replace the default behavior and send OTPs instead. [source](https://github.com/better-auth/better-auth/issues/5936) For registration, if you want only OTP emails, remove sendVerificationEmail from your emailVerification config and rely on the emailOTP plugin with overrideDefaultEmailVerification: true. [source](https://github.com/better-auth/better-auth/issues/5936) For change email, Better Auth doesn't natively support sending OTPs to the new email—the default flow sends a link. If you want to send an OTP for email change, you'll need a custom implementation: generate/store an OTP for the new email, send it in your own handler, and create an endpoint to verify the OTP before updating the user's email. [source](https://github.com/better-auth/better-auth/discussions/5540) If you want to send both a verification link and OTP in the same email, you can manually generate an OTP inside your sendVerificationEmail function and include both in the email content, but this requires handling OTP generation and storage yourself. There's an open PR to support this natively, but it's not available in 1.4.6 yet. [source](https://github.com/better-auth/better-auth/pull/5533) Let me know if you want example code for any of these workarounds. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/6a113563-5933-450b-93c8-fa7a13555a77?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/6a113563-5933-450b-93c8-fa7a13555a77?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/6a113563-5933-450b-93c8-fa7a13555a77?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/6a113563-5933-450b-93c8-fa7a13555a77?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/6a113563-5933-450b-93c8-fa7a13555a77?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/6a113563-5933-450b-93c8-fa7a13555a77?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/6a113563-5933-450b-93c8-fa7a13555a77?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=The%20reason%20your%20OTP%20emails%20aren%27t%20sent%20when%20sendVerificationEmail%20is%20present%20is%20because%20Better%20Auth%27s%20config%20merging%20gives%20priority%20to%20your%20explicit%20sendVerificationEmail%20function%20over%20the%20emailOTP%20plugin%27s%20overrideDefaultEmailVerification%20option.%20So%2C%20during%20registration%20and%20email%20change%2C%20the%20system%20always%20calls%20your%20sendVerificationEmail%20and%20sends%20a%20verification%20link%2C%20not%20an%20OTP%20code.%20The%20override%20only%20works%20if%20sendVerificationEmail%20is%20not%20defined%20in%20your%20config%E2%80%94then%20the%20plugin%20can%20replace%20the%20default%20behavior%20and%20send%20OTPs%20instead.%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/5936%29%0A%0AFor%20registration%2C%20if%20you%20want%20only%20OTP%20emails%2C%20remove%20sendVerificationEmail%20from%20your%20emailVerification%20config%20and%20rely%20on%20the%20emailOTP%20plugin%20with%20overrideDefaultEmailVerification%3A%20true.%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/5936%29%0A%0AFor%20change%20email%2C%20Better%20Auth%20doesn%27t%20natively%20support%20sending%20OTPs%20to%20the%20new%20email%E2%80%94the%20default%20flow%20sends%20a%20link.%20If%20you%20want%20to%20send%20an%20OTP%20for%20email%20change%2C%20you%27ll%20need%20a%20custom%20implementation%3A%20generate/store%20an%20OTP%20for%20the%20new%20email%2C%20send%20it%20in%20your%20own%20handler%2C%20and%20create%20an%20endpoint%20to%20verify%20the%20OTP%20before%20updating%20the%20user%27s%20email.%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/discussions/5540%29%0A%0AIf%20you%20want%20to%20send%20both%20a%20verification%20link%20and%20OTP%20in%20the%20same%20email%2C%20you%20can%20manually%20generate%20an%20OTP%20inside%20your%20sendVerificationEmail%20function%20and%20include%20both%20in%20the%20email%20content%2C%20but%20this%20requires%20handling%20OTP%20generation%20and%20storage%20yourself.%20There%27s%20an%20open%20PR%20to%20support%20this%20natively%2C%20but%20it%27s%20not%20available%20in%201.4.6%20yet.%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/pull/5533%29%0A%0ALet%20me%20know%20if%20you%20want%20example%20code%20for%20any%20of%20these%20workarounds.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/6644)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2494