OTP Mailer
OTP Mailer / Overview
TypeScript Self-hosted Vercel ready MIT License

OTP Mailer

A lightweight, self-hosted email verification service. Send a numeric OTP the user types in, or a one-click magic link — two completely independent flows backed by a single Express API.

It handles generation, delivery, expiry, and verification so your backend only needs a single API call to start a flow. Built-in rate limiting protects against abuse, and webhook callbacks let you react instantly when a magic link is clicked.

All endpoints live under the /api prefix. Set Content-Type: application/json on every POST request.

Recommended reading order:

  1. Read Verification Flows to pick the right mode
  2. Follow Quick Start to send your first email
  3. Use the API Reference endpoints with live Try It panels
  4. Configure Environment Variables for production

Verification Flows

Two independent flows. Choose based on your UX preference — they never mix. A code email never contains a button; a link email never contains a code.

Code Flow

1Call POST /api/otp/generate
2Service emails a numeric code
3User types it, call POST /api/otp/verify

Magic Link Flow

1Call POST /api/otp/send-link
2Service emails a verify button
3User clicks it — confirmation page shown

Quick Start

Clone, configure, run — three commands to your first verification email.

1 — Clone and install

bash
git clone https://github.com/pooraddyy/otp-mailer.git
cd otp-mailer
npm install

2 — Configure environment

bash
cp sample.env .env
# Fill in MONGODB_URI, GMAIL_USER, GMAIL_PASS

3 — Run locally

bash
npm run dev
# Listening on http://localhost:5001

Send your first email

bash
# Code flow
curl -X POST http://localhost:5001/api/otp/generate   -H "Content-Type: application/json"   -d '{"email":"you@example.com","organization":"Acme"}'

# Magic link flow
curl -X POST http://localhost:5001/api/otp/send-link   -H "Content-Type: application/json"   -d '{"email":"you@example.com","organization":"Acme"}'

Send OTP Code

Generates a new OTP and sends it to the recipient's inbox as a numeric or alphanumeric code.

POST /api/otp/generate Send a verification code to an email address

Request Body

FieldTypeRequiredDescription
emailstringrequiredRecipient email address
typenumeric | alphanumericoptionalOTP character set. Default: numeric
organizationstringoptionalSender brand name in the email. Default: Verification
subjectstringoptionalEmail subject line. Default: Your verification code

Response — 200

json
{
  "message": "Verification code sent to your email",
  "mode": "code",
  "requestId": "8f0a2c...",
  "validityMinutes": 5
}
Try it Live request

Sends a one-click "Verify my email" button. Optionally pass a webhookUrl to receive an instant POST callback when the user clicks.

POST /api/otp/send-link Send a magic link verification email

Request Body

FieldTypeRequiredDescription
emailstringrequiredRecipient email address
organizationstringoptionalSender brand name. Default: Verification
subjectstringoptionalEmail subject. Default: Verify your email
webhookUrlstringoptionalYour endpoint to receive a POST when the user verifies. See Webhooks

Response — 200

json
{
  "message": "Verification link sent to your email",
  "mode": "link",
  "requestId": "b7e3...",
  "validityMinutes": 5,
  "webhookRegistered": true
}
Try it Live request

Verify by Code

Validates the OTP code the user typed. Single-use — consumed on first success.

POST /api/otp/verify Validate the numeric OTP code

Request Body

json
{ "email": "user@example.com", "otp": "123456" }

Response — 200

json
{ "message": "Email verified successfully", "verified": true }
Try it Live request

Browser-facing endpoint embedded in magic link emails. Validates the token, marks the record as verified, and renders a styled confirmation page. You do not call this from your backend — the user's email client does.

GET /api/otp/verify-link?token=… Validate a magic link token
This endpoint renders an HTML confirmation page — not JSON. The URL is constructed automatically and embedded in the link email as a button target.

Query Parameters

ParameterTypeRequiredDescription
tokenstringrequiredThe single-use base64url token from the verification email
Try it Opens in new tab

Check Status

Returns the verification status of a request. Use this to poll in the magic link flow, or as a fallback if your webhook misses a delivery.

GET /api/otp/status/:requestId Get verification status by request ID

Response — 200

json
{
  "found": true,
  "verified": false,
  "expired": false,
  "email": "user@example.com"
}
FieldMeaning
foundA request with this ID exists in the database
verifiedThe user has clicked the link or entered the correct code
expiredThe validity window has elapsed
Try it Live request

Rate Limiting

Every request to /api/otp/generate and /api/otp/send-link goes through an in-memory rate limiter. Both IP address and email address are tracked independently.

If either IP or email exceeds the limit, the request is rejected with 429. Configure with RATE_LIMIT_MAX_REQUESTS and RATE_LIMIT_WINDOW_MINUTES.

429 Response

json
{
  "error": "Too many requests. Try again in 12 minutes.",
  "retryAfterSeconds": 720
}
Test Rate Limit Fires 6 rapid requests

Webhooks

Pass a webhookUrl to POST /api/otp/send-link and the service fires a POST to your server the instant the user clicks the verify button.

Payload delivered to your server

json
{
  "event": "email.verified",
  "email": "user@example.com",
  "requestId": "b7e3a2f1...",
  "verifiedAt": "2026-05-26T10:30:00.000Z"
}
  • Fire-and-forget — the confirmation page does not wait for your server to respond
  • 5-second timeout. Slow or unreachable URLs are aborted and logged server-side
  • Only http:// and https:// URLs are accepted
  • No automatic retries — use GET /api/otp/status/:requestId as a reliable fallback
Test Webhook Use requestbin.com for testing

Environment Variables

All variables live in sample.env. Copy it to .env for local development. For Vercel, add them under Settings → Environment Variables.

VariableRequiredDefaultDescription
MONGODB_URIrequiredMongoDB connection string
GMAIL_USERrequiredSender Gmail address. Must have an App Password enabled
GMAIL_PASSrequired16-character Gmail App Password
OTP_VALIDITY_PERIOD_MINUTESrequired5How long a code or link token stays valid
OTP_SIZErequired6Number of characters in the OTP
APP_BASE_URLoptionalautoPublic URL for magic links. Falls back to $VERCEL_URL then request host
RATE_LIMIT_MAX_REQUESTSoptional5Max requests per window per IP or email
RATE_LIMIT_WINDOW_MINUTESoptional15Length of the rate-limit rolling window in minutes
BLOCK_KEYWORDS_RULESoptionalComma-separated keywords that auto-reject requests
ALLOWED_DOMAINSoptionalComma-separated domain allow-list. Empty = all accepted
DOCS_API_URLoptionalautoBase URL used by live Try It panels on the docs page
PORTlocal only5001Dev server port. Vercel ignores this

Error Reference

All errors return { "error": "message" }.

StatusErrorCause
400 Invalid email Missing or malformed email field
400 Spam detected IP or email is blocklisted, or body contains a blocked keyword
400 Maximum attempts reached Same email hit the 3-attempt limit within the validity window
400 Invalid OTP Code is wrong, expired, or already used
400 Verification link is invalid or has expired No matching token found, or token has expired
429 Too many requests Rate limit exceeded. Check retryAfterSeconds in the response
500 Internal server error Unexpected database or runtime error

OTP Mailer · MIT License · GitHub