Two-factor authentication is one of those features that sounds simple until you're in the middle of building it. OTP generation, expiry windows, rate limiting, delivery channel fallback — it adds up fast. In this guide we'll build a complete 2FA flow using Wasika in under 50 lines of application code.

The flow

1. User submits their phone number. 2. We generate a 6-digit OTP, store it with a 10-minute TTL, and send it via SMS. If SMS fails (e.g. the number is a VoIP line), we fall back to email automatically. 3. User submits the OTP. We verify it's valid and unexpired, then mark the session authenticated.

Generating and sending the OTP

javascript
import { Wasika } from "@wasika/sdk";
import { createClient } from "redis";
import crypto from "crypto";

const wasika = new Wasika({ apiKey: process.env.WSK_KEY });
const redis = createClient();

export async function sendOtp(phoneNumber: string, email: string) {
  const otp = crypto.randomInt(100_000, 999_999).toString();
  const key = \`otp:${phoneNumber}\`;

  await redis.set(key, otp, { EX: 600 }); // 10-minute TTL

  await wasika.messages.send({
    channel: "sms",
    to: phoneNumber,
    body: \`Your Wasika verification code is ${otp}. Expires in 10 minutes.\`,
    fallback: [{ channel: "email", to: email,
      subject: "Your verification code",
      body: \`Your code is ${otp}\` }],
  });
}

The fallback field does the heavy lifting. If the SMS fails at the carrier level, Wasika automatically sends the email instead — no retry logic in your application code, no second API call.

Verifying the OTP

javascript
export async function verifyOtp(phoneNumber: string, submitted: string) {
  const key = \`otp:${phoneNumber}\`;
  const stored = await redis.get(key);

  if (!stored || stored !== submitted) {
    throw new Error("Invalid or expired code");
  }

  await redis.del(key); // single-use
  return true;
}

Rate limiting

Don't forget to rate-limit the send endpoint — at minimum, one OTP per phone number per minute, and a hard cap of 5 per hour. Redis makes this straightforward with a counter key and a sliding window TTL. The verification endpoint should also lock out after 5 failed attempts to prevent brute-force.

That's the core. In production you'd add the rate limiting middleware, hook the webhook to mark delivery confirmed, and surface a 'resend' button on the frontend after a 60-second cooldown. Total application logic: well under 50 lines.