SMSTunnel for Better Auth

Phone number sign-in codes from your own phone

npm package · phoneNumber plugin
@narcisbodea/smstunnel-otp

One function for sendOTP

Better Auth's phoneNumber plugin asks you how to send its codes. smstunnelSendOTP() answers: it hands each code to your SMSTunnel account and your own Android phone sends it from its own number, so people get it from a number they know.

Before you start

  1. 1. Install the SMSTunnel app from Google Play on your Android phone and sign in.
  2. 2. In your dashboard, open Apps → Add an app, pick Sign-in codes, name it (for example "Better Auth") and click Create the key.
  3. 3. Copy the key right away: it's shown only once. You can stop or delete the app there anytime, and Better Auth can't send texts anymore.

Get the steps with your key in them

It opens Add an app with Sign-in codes picked. Once the key is made, the dashboard shows every step and command with the key already filled in, ready to copy.

Keep the key in your platform's secrets, never in code that runs in the browser. Codes must be 4 to 10 digits: the default 6-digit codes are fine; codes with letters aren't accepted.

1. Install

npm install @narcisbodea/smstunnel-otp

No runtime dependencies: it runs in Node 18+, Deno, Bun and Cloudflare Workers.

2. Plug it into the phoneNumber plugin

auth.ts
import { betterAuth } from 'better-auth';
import { phoneNumber } from 'better-auth/plugins';
import { smstunnelSendOTP } from '@narcisbodea/smstunnel-otp';

const apiKey = process.env.SMSTUNNEL_API_KEY!;

export const auth = betterAuth({
  plugins: [
    phoneNumber({
      sendOTP: smstunnelSendOTP({ apiKey, template: 'Your Acme code is {code}' }),
      sendPasswordResetOTP: smstunnelSendOTP({ apiKey, template: 'Your Acme password reset code is {code}' }),
    }),
  ],
});
  • Better Auth runs sendOTP in the background when it can, so sign-in doesn't wait for the send.
  • If people type local numbers, add defaultCountryCode to the smstunnelSendOTP() options (for example '44'), or validate numbers with the plugin's phoneNumberValidator.
  • Other options: deviceId (send from one phone of your account), serverUrl (self-hosted SMSTunnel) and timeoutMs (default 10000).
  • If you change the code length, keep it between 4 and 10 digits.

When a code can't be sent

The function rejects with a SmsTunnelOtpError:

Field Meaning
status HTTP status from SMSTunnel; 0 for a timeout, a network error or a refused argument
code SMSTunnel's own code when it sends one (for example PERMISSION_REQUIRED), else UNAUTHORIZED (401), FORBIDDEN (403), BAD_REQUEST (400), RATE_LIMITED (429), SERVER_ERROR (5xx), TIMEOUT, NETWORK_ERROR, INVALID_ARGUMENT, MESSAGE_TOO_LONG
retryable true for 429, 5xx, timeouts and network errors
message What went wrong, in words

Worth knowing

  • Codes have their own allowance. They go through SMSTunnel's code endpoint, POST /api/v1/sms/send-2fa, which counts them apart from your other texts. See Pricing.
  • What counts as a code: 4 to 10 digits standing on their own, like 482913, or two groups of 3 digits, like 123-456. A text without one is refused. Details
  • One number per text, up to 320 characters. The package refuses a longer text before calling SMSTunnel.
  • 30 sends a minute. Past that, SMSTunnel answers 429 and asks to retry a little later.
  • Phone offline? The code waits and goes out as soon as the phone is back.
  • Nothing in the package logs codes or keys.

Build it with an AI assistant

Copy this prompt into Claude, ChatGPT, Cursor or Copilot and replace the part in [brackets]. Don't paste your API key into the chat: the prompt doesn't need it.

Help me send the phone number sign-in codes of my Better Auth app from my own phone, with SMSTunnel.

About SMSTunnel (https://smstunnel.io): my own apps and tools hand text messages (SMS) to my SMSTunnel account, and my own Android phone sends them from my own number, on my own SIM and plan. The phone runs the SMSTunnel app from Google Play (https://play.google.com/store/apps/details?id=io.smstunnel.app), signed in to my account.

What I want: [describe what should send a text, to whom, and when]

How it works:
- Install: npm install @narcisbodea/smstunnel-otp (no dependencies; Node 18+, Deno, Bun, Cloudflare Workers).
- Better Auth's phoneNumber plugin takes sendOTP and sendPasswordResetOTP functions; smstunnelSendOTP() builds them:
import { betterAuth } from 'better-auth';
import { phoneNumber } from 'better-auth/plugins';
import { smstunnelSendOTP } from '@narcisbodea/smstunnel-otp';

const apiKey = process.env.SMSTUNNEL_API_KEY!;

export const auth = betterAuth({
  plugins: [
    phoneNumber({
      sendOTP: smstunnelSendOTP({ apiKey, template: 'Your Acme code is {code}' }),
      sendPasswordResetOTP: smstunnelSendOTP({ apiKey, template: 'Your Acme password reset code is {code}' }),
    }),
  ],
});
- smstunnelSendOTP options: apiKey (required), template (text with {code}; default "Your verification code is {code}"), defaultCountryCode (for numbers typed without + or 00, for example '44'), deviceId (send from one phone of my account), serverUrl (self-hosted SMSTunnel), timeoutMs (default 10000).
- It rejects with SmsTunnelOtpError when the code can't be sent: fields status, code (e.g. PERMISSION_REQUIRED, UNAUTHORIZED, RATE_LIMITED, TIMEOUT, MESSAGE_TOO_LONG), retryable (true for 429, 5xx, timeouts, network errors) and message.
- Better Auth runs sendOTP in the background when it can, so sign-in doesn't wait for the send. Numbers can also be checked with the plugin's phoneNumberValidator.
- Codes go through SMSTunnel's POST https://smstunnel.io/api/v1/sms/send-2fa: the text must contain the code as 4 to 10 digits (keep the code length in that range) and can be up to 320 characters. Codes have their own daily allowance. If the phone is offline, the code waits and goes out when it is back.
- The API key comes from my SMSTunnel dashboard (https://dashboard.smstunnel.io): Apps → Add an app → Create the key. It is shown only once and can send texts and check whether they went out.

Rules for your answer:
- Keep the API key in an environment variable or the platform's secret store (for example SMSTUNNEL_API_KEY). Never put it in source code, in a repository, or in code that runs in a browser or a mobile app: call SMSTunnel from a server. I will not paste my key in this chat; use a placeholder.
- Validate phone numbers before sending (international format, like +14155550123) and skip or report the invalid ones.
- Handle errors: log or show what went wrong, retry only after 429, 5xx or network errors and only a few times, and never send the same text twice.
- Only send texts people expect from me: alerts to myself, reminders, sign-in codes, updates they asked for. No unsolicited texts.

Better Auth is a trademark of its owner. SMSTunnel is a third-party integration and is not affiliated with, endorsed by, or sponsored by Better Auth.

Sign-in codes for other platforms

Another platform? sendCode() from the same package sends any code: see its README.

No phone paired yet?

Install the Android app and sign in with your account, and you're ready in about a minute.

Get it on Google Play