mbedded in a template that includes at least one variable (the code itself). Templates are submitted through Meta Business Suite or your BSP dashboard. Approval typically takes 24–48 hours. Once approved, the template ID becomes a static configuration value in your application.
Step 2: Session Initialization (send)
When a user requests verification, your backend generates a cryptographically secure 6-digit code, binds it to a unique session identifier, and forwards the payload to your chosen provider. The provider returns a session reference that your system stores temporarily (Redis, DynamoDB, or in-memory cache with TTL). The response confirms acceptance, not delivery. This distinction is intentional and correct.
Step 3: Verification (verify)
The client submits the session identifier and the user-entered code. Your backend validates the code against the stored session, checks expiration, enforces attempt limits, and returns a success or failure response. Successful verification implicitly confirms delivery. No webhook, status callback, or polling loop is required.
Architecture Decisions & Rationale
- Provider Abstraction: Direct Meta integration requires handling OAuth flows, phone number registration, template versioning, and webhook signature verification. Using a BSP or lightweight API wrapper abstracts these operational burdens while maintaining compliance. Abstraction also prevents vendor lock-in and simplifies fallback routing.
- Stateless Verification: Storing only the hashed code and session metadata reduces memory footprint and eliminates the need for delivery state machines. The verification endpoint becomes the single source of truth.
- Idempotency Keys: Network retries during
send requests can cause duplicate messages. Including an idempotency key in the request payload ensures the provider processes the request exactly once, preventing user confusion and cost inflation.
- TTL-Driven Cleanup: Sessions must expire within 5–10 minutes. Short lifespans reduce brute-force attack windows and align with user attention spans. Automated cleanup via cache expiration or scheduled jobs prevents stale data accumulation.
Implementation Example (TypeScript)
import { randomInt } from 'crypto';
import { createHash } from 'crypto';
interface VerificationSession {
sessionId: string;
phoneNumber: string;
codeHash: string;
createdAt: number;
ttlMs: number;
maxAttempts: number;
attemptsUsed: number;
}
interface VerificationProvider {
initiateSession(phone: string, templateId: string, code: string): Promise<{ sessionId: string; status: string }>;
confirmSession(sessionId: string, userCode: string): Promise<{ verified: boolean; reason?: string }>;
}
class PhoneVerificationOrchestrator {
private sessions: Map<string, VerificationSession> = new Map();
private provider: VerificationProvider;
private templateId: string;
constructor(provider: VerificationProvider, templateId: string) {
this.provider = provider;
this.templateId = templateId;
}
async createVerificationSession(phoneNumber: string): Promise<{ sessionId: string; status: string }> {
const sessionId = `vfy_${Date.now()}_${randomInt(100000, 999999)}`;
const code = String(randomInt(100000, 999999));
const codeHash = createHash('sha256').update(code).digest('hex');
const ttlMs = 600_000; // 10 minutes
const session: VerificationSession = {
sessionId,
phoneNumber,
codeHash,
createdAt: Date.now(),
ttlMs,
maxAttempts: 5,
attemptsUsed: 0
};
this.sessions.set(sessionId, session);
const result = await this.provider.initiateSession(phoneNumber, this.templateId, code);
// Schedule automatic cleanup
setTimeout(() => this.sessions.delete(sessionId), ttlMs);
return result;
}
async confirmVerification(sessionId: string, userCode: string): Promise<{ verified: boolean; reason?: string }> {
const session = this.sessions.get(sessionId);
if (!session) {
return { verified: false, reason: 'SESSION_EXPIRED_OR_INVALID' };
}
if (Date.now() - session.createdAt > session.ttlMs) {
this.sessions.delete(sessionId);
return { verified: false, reason: 'SESSION_EXPIRED' };
}
if (session.attemptsUsed >= session.maxAttempts) {
this.sessions.delete(sessionId);
return { verified: false, reason: 'MAX_ATTEMPTS_EXCEEDED' };
}
session.attemptsUsed++;
const inputHash = createHash('sha256').update(userCode).digest('hex');
if (inputHash === session.codeHash) {
this.sessions.delete(sessionId);
return { verified: true };
}
return { verified: false, reason: 'INVALID_CODE' };
}
}
This implementation isolates session lifecycle management from provider communication, enforces cryptographic code storage, and guarantees automatic expiration. The VerificationProvider interface allows seamless swapping between BSPs without modifying core verification logic.
Pitfall Guide
1. Unofficial WhatsApp Automation
Explanation: Using web-scraping libraries or unofficial clients to bypass API pricing violates Meta's platform policies. Accounts are rapidly flagged, suspended, or permanently banned. Deliverability collapses as Meta blocks traffic from unverified sources.
Fix: Always route through the official WhatsApp Business Platform or a certified BSP. Accept the pricing structure as the cost of reliable, compliant delivery.
2. Skipping Template Pre-Approval
Explanation: Business-initiated messages require pre-approved templates. Attempting to send dynamic OTP codes without a registered template results in immediate rejection or delivery failure.
Fix: Register your OTP template in Meta Business Suite before deployment. Include a single variable placeholder for the code. Test in the sandbox environment before moving to production.
3. Webhook-Driven Delivery Tracking
Explanation: Engineering teams often build complex webhook handlers to track message status (sent, delivered, read). For OTP, this adds latency, infrastructure cost, and failure surfaces without improving verification accuracy.
Fix: Treat the verification endpoint as the delivery proof. Remove webhook dependencies entirely. If delivery analytics are required, aggregate them separately from the verification flow.
4. Missing Rate Limiting & Attempt Caps
Explanation: Without request throttling and attempt limits, attackers can brute-force codes or trigger mass message generation, inflating costs and degrading service availability.
Fix: Implement sliding-window rate limits on the send endpoint per IP/phone number. Enforce a maximum of 3–5 verification attempts per session. Lock sessions after threshold breach.
5. Client-Side Secret Exposure
Explanation: Embedding API keys or provider credentials in frontend code or mobile apps exposes them to reverse engineering. Attackers can drain budgets or impersonate your service.
Fix: Restrict all provider communication to backend services. Use environment variables, secret managers, or IAM roles. Never expose authentication tokens to client environments.
6. Inadequate Session Expiration
Explanation: Long-lived sessions increase brute-force viability and clutter storage. Short-lived sessions without cleanup cause memory leaks in stateful servers.
Fix: Set TTL to 5–10 minutes. Use cache expiration mechanisms (Redis EXPIRE, DynamoDB TTL) or background cleanup jobs. Never rely solely on application-level timers for production workloads.
7. No Fallback Routing
Explanation: Assuming WhatsApp is universally available leads to conversion drop-offs for users with restricted data plans, regional outages, or unregistered numbers.
Fix: Implement a fallback strategy. After two failed WhatsApp attempts or a timeout, route the next verification via SMS. Log channel performance to optimize routing rules over time.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-volume Brazil/LatAm onboarding | WhatsApp Business API via BSP | Highest read rate (~98%), lowest cost (~R$ 0.03), native UX | Low per-message cost; scales efficiently |
| Global user base with mixed connectivity | WhatsApp primary + SMS fallback | Covers regions with WhatsApp restrictions; maintains conversion | Moderate; SMS fallback adds ~R$ 0.10 per fallback |
| Strict compliance & audit requirements | Direct Meta integration + internal logging | Full control over data flow, template versioning, and delivery logs | Higher engineering overhead; no BSP markup |
| Budget-constrained MVP | Lightweight BSP with free verification tier | Minimizes upfront cost; validates funnel before scaling | Near-zero verification cost; pay only for send |
Configuration Template
// verification.config.ts
export const verificationConfig = {
provider: {
type: 'bsp',
endpoint: 'https://api.provider.example.com/v1/verify',
apiKey: process.env.WHATSAPP_API_KEY,
timeoutMs: 5000
},
session: {
ttlMs: 600_000,
maxAttempts: 5,
cleanupIntervalMs: 300_000
},
rateLimit: {
windowMs: 60_000,
maxRequestsPerWindow: 3,
keyStrategy: 'phone_number'
},
fallback: {
enabled: true,
channel: 'sms',
triggerAfterFailedAttempts: 2,
templateId: 'sms_otp_v1'
},
security: {
codeLength: 6,
hashAlgorithm: 'sha256',
idempotencyHeader: 'X-Request-Id'
}
};
Quick Start Guide
- Register Template: Submit an OTP template with one variable to Meta Business Suite. Wait for approval (typically 24–48 hours).
- Initialize Provider: Install your chosen BSP SDK or configure HTTP client with API credentials. Set environment variables securely.
- Deploy Orchestrator: Integrate the
PhoneVerificationOrchestrator class into your auth service. Configure TTL, attempt limits, and rate limiting per the template above.
- Test Flow: Trigger a verification request, capture the session ID, submit the correct code, and confirm success. Validate expiration and attempt limits with negative tests.
- Monitor Metrics: Track session creation rate, verification success rate, average time-to-verify, and fallback trigger frequency. Adjust TTL and rate limits based on observed patterns.
This architecture delivers a production-ready verification system that prioritizes reliability, cost efficiency, and security. By treating verification as the delivery proof, abstracting provider dependencies, and enforcing strict session lifecycle controls, engineering teams can eliminate unnecessary complexity while improving conversion metrics across Brazilian and Latin American markets.