OTP Emails
Send One-Time Password (OTP) verification emails with Metigan. Support for custom templates, automatic rate limiting, and priority delivery for authentication flows.
Overview
OTP emails are critical for user authentication, account verification, and password recovery. Metigan provides a dedicated POST /api/otp endpoint optimized for OTP delivery with:
- Priority delivery - OTP emails are sent with highest priority
- Rate limiting - Built-in protection against abuse
- Template support - Use custom templates or default design
- Variable substitution - Dynamic code and app name replacement
- Idempotency - Prevent duplicate sends with idempotency keys
Quick Start
Send your first OTP email in seconds using the default template:
import Metigan from 'metigan';
const metigan = new Metigan({
apiKey: process.env.METIGAN_API_KEY!
});
// Send OTP with default template
const result = await metigan.otp.send({
to: 'user@example.com',
from: 'auth@yourdomain.com',
code: '123456',
appName: 'MyApp',
expiresInMinutes: 5
});
console.log(result);
// { success: true, queued: true, emailId: "otp-...", trackingId: "otp-..." }OTP Without Template (Default)
When you don't specify a templateId, Metigan uses a clean, professional default template that displays your OTP code prominently.
const Metigan = require('metigan');
const metigan = new Metigan({
apiKey: process.env.METIGAN_API_KEY
});
async function sendVerificationCode(userEmail) {
// Generate a random 6-digit code
const code = Math.floor(100000 + Math.random() * 900000).toString();
const result = await metigan.otp.send({
to: userEmail,
from: 'noreply@myapp.com',
code: code,
appName: 'MyApp',
subject: 'Your verification code', // Optional custom subject
expiresInMinutes: 10
});
if (result.success) {
console.log('OTP sent! Tracking ID:', result.trackingId);
// Store code in your database/cache for verification
return { success: true, code }; // Don't return code in production!
} else {
console.error('Failed to send OTP:', result.error);
return { success: false };
}
}
sendVerificationCode('user@example.com');Default Template Preview
The default OTP template displays:
- Title: "Seu código de verificação" (Your verification code)
- App name context message
- Large, spaced OTP code for easy reading
- Expiration notice (if provided)
- Security disclaimer
OTP With Custom Template
Use your own branded templates for OTP emails. Templates support dynamic variables that are automatically replaced when sending.
Step 1: Create an OTP Template
Create a template with OTP-specific variables. Supported variables:
| Variable | Description | Example |
|---|---|---|
| {{code}} | The OTP code | 123456 |
| {{appName}} | Your application name | MyApp |
| {{expiresInMinutes}} | Code expiration time | 10 |
import Metigan from 'metigan';
const metigan = new Metigan({
apiKey: process.env.METIGAN_API_KEY!
});
// Create a branded OTP template
const template = await metigan.templates.create({
name: 'Branded OTP Template',
subject: '{{appName}} - Verification Code',
content: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Arial, sans-serif; background-color: #f5f5f5;">
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 600px; margin: 0 auto; background: white;">
<tr>
<td style="padding: 40px 30px; text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<h1 style="margin: 0; color: white; font-size: 28px;">{{appName}}</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin: 0 0 20px 0; color: #333; font-size: 24px;">Verification Code</h2>
<p style="margin: 0 0 30px 0; color: #666; font-size: 16px; line-height: 1.6;">
Enter the code below to verify your identity:
</p>
<!-- OTP Code Box -->
<div style="background: #f8f9fa; border: 2px dashed #667eea; border-radius: 12px; padding: 30px; text-align: center; margin: 30px 0;">
<span style="font-size: 42px; font-weight: bold; letter-spacing: 12px; color: #333; font-family: 'Courier New', monospace;">
{{code}}
</span>
</div>
<p style="margin: 20px 0; color: #999; font-size: 14px; text-align: center;">
⏱️ This code expires in <strong>{{expiresInMinutes}} minutes</strong>
</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="margin: 0; color: #999; font-size: 13px; line-height: 1.5;">
If you didn't request this code, please ignore this email or contact our support team.
</p>
</td>
</tr>
<tr>
<td style="padding: 20px 30px; background: #f8f9fa; text-align: center;">
<p style="margin: 0; color: #999; font-size: 12px;">
© {{appName}} - Sent via Metigan
</p>
</td>
</tr>
</table>
</body>
</html>
`
});
console.log('Template ID:', template.id);
// Save this ID for sending OTPsStep 2: Send OTP Using Template
Reference your template by ID when sending. Variables will be automatically replaced:
import Metigan from 'metigan';
const metigan = new Metigan({
apiKey: process.env.METIGAN_API_KEY!
});
const BRANDED_OTP_TEMPLATE_ID = 'your_template_id_here';
async function sendBrandedOtp(userEmail: string) {
const code = crypto.randomInt(100000, 999999).toString();
const result = await metigan.otp.send({
to: userEmail,
from: 'security@myapp.com',
code: code,
appName: 'MyApp',
expiresInMinutes: 10,
templateId: BRANDED_OTP_TEMPLATE_ID // 🎨 Use custom template
});
if (result.success) {
console.log('✅ Branded OTP sent!');
console.log('Tracking ID:', result.trackingId);
return { success: true, code };
} else {
console.error('❌ Failed:', result.error);
return { success: false };
}
}
// Send branded OTP
sendBrandedOtp('user@example.com');- • Make the OTP code large and easy to read (42px+ font size)
- • Use letter-spacing for better digit separation
- • Include expiration time prominently
- • Add security disclaimers
- • Test on mobile devices - most OTPs are read on phones
API Reference
POST /api/otp - Send an OTP email
| Parameter | Type | Required | Description |
|---|---|---|---|
| to | string | Yes | Recipient email address (also accepts email) |
| from | string | Yes | Sender email address (must be verified domain) |
| code | string | Yes | The OTP code (max 12 characters) |
| appName | string | No | Your application name (default: "Metigan") |
| subject | string | No | Custom subject line (default: "[appName] - Seu código de verificação") |
| expiresInMinutes | number | No | Code expiration time to display in email |
| templateId | string | No | Custom template ID to use instead of default |
| idempotencyKey | string | No | Unique key to prevent duplicate sends |
Rate Limits
OTP endpoints have built-in rate limiting to prevent abuse. Limits are per recipient, per user:
| Plan | OTPs per recipient | Time window |
|---|---|---|
| Free | 3 | 5 minutes |
| Pro | 5 | 5 minutes |
| Growth | 6 | 5 minutes |
| Business | 8 | 5 minutes |
If you exceed the rate limit, you'll receive a 429 Too Many Requests response. Implement exponential backoff and show users a "wait before requesting new code" message.
Security Best Practices
✅ Do
- • Use 6-8 digit numeric codes
- • Set short expiration (5-10 minutes)
- • Store codes hashed in your database
- • Invalidate code after successful verification
- • Use idempotency keys for retries
- • Log verification attempts
❌ Don't
- • Never return the OTP code in API responses
- • Don't use predictable codes
- • Avoid very long expiration times
- • Don't allow unlimited verification attempts
- • Never log actual OTP codes
- • Don't reuse the same code
Complete Example
Here's a full implementation of an OTP verification flow:
import Metigan from 'metigan';
import crypto from 'crypto';
import Redis from 'ioredis';
const metigan = new Metigan({
apiKey: process.env.METIGAN_API_KEY!
});
const redis = new Redis(process.env.REDIS_URL!);
const OTP_EXPIRY_SECONDS = 600; // 10 minutes
const MAX_VERIFY_ATTEMPTS = 5;
// Generate secure OTP
function generateOtp(): string {
return crypto.randomInt(100000, 999999).toString();
}
// Hash OTP for storage
function hashOtp(otp: string): string {
return crypto.createHash('sha256').update(otp).digest('hex');
}
// Send OTP
export async function sendOtp(email: string, purpose: 'login' | 'register' | 'reset') {
const code = generateOtp();
const hashedCode = hashOtp(code);
const key = `otp:${purpose}:${email}`;
const attemptsKey = `otp:attempts:${email}`;
// Store hashed OTP with expiry
await redis.setex(key, OTP_EXPIRY_SECONDS, hashedCode);
await redis.del(attemptsKey); // Reset attempts on new OTP
// Send via Metigan
const result = await metigan.otp.send({
to: email,
from: 'security@myapp.com',
code: code,
appName: 'MyApp',
expiresInMinutes: Math.floor(OTP_EXPIRY_SECONDS / 60),
idempotencyKey: `${purpose}-${email}-${Date.now()}`
});
return {
success: result.success,
message: result.success
? 'Verification code sent to your email'
: 'Failed to send verification code'
};
}
// Verify OTP
export async function verifyOtp(email: string, code: string, purpose: string) {
const key = `otp:${purpose}:${email}`;
const attemptsKey = `otp:attempts:${email}`;
// Check attempts
const attempts = await redis.incr(attemptsKey);
await redis.expire(attemptsKey, OTP_EXPIRY_SECONDS);
if (attempts > MAX_VERIFY_ATTEMPTS) {
await redis.del(key); // Invalidate OTP after too many attempts
return { success: false, error: 'Too many attempts. Request a new code.' };
}
// Get stored hash
const storedHash = await redis.get(key);
if (!storedHash) {
return { success: false, error: 'Code expired or not found' };
}
// Verify
const inputHash = hashOtp(code);
if (inputHash !== storedHash) {
return {
success: false,
error: 'Invalid code',
attemptsRemaining: MAX_VERIFY_ATTEMPTS - attempts
};
}
// Success - invalidate OTP
await redis.del(key);
await redis.del(attemptsKey);
return { success: true, message: 'Verification successful' };
}
// Usage in Express route
app.post('/api/auth/send-otp', async (req, res) => {
const { email, purpose } = req.body;
const result = await sendOtp(email, purpose);
res.json(result);
});
app.post('/api/auth/verify-otp', async (req, res) => {
const { email, code, purpose } = req.body;
const result = await verifyOtp(email, code, purpose);
res.json(result);
});Learn more about Transactional Emails or explore the Templates API for creating custom email designs.