DEV Community

WALoops
WALoops

Posted on

Phone OTP login without SMS DLT headaches: sending the code over WhatsApp

Building phone-based authentication for products in India usually involves navigating telecom compliance before writing any backend code. Under the Telecom Regulatory Authority of India (TRAI) regulations, sending transactional SMS requires registering your business on a Distributed Ledger Technology (DLT) portal, submitting legal entity documents, registering telemarketers, reserving SMS headers (Sender IDs), and submitting explicit message templates.

Each message template must match exact variable formats. A mismatch in punctuation or whitespace can cause the telecom gateway to reject the message silently. Beyond registration friction, SMS delivery across Indian telecom operators faces unpredictable routing delays, drop-offs during peak traffic, and international toll fraud (OTP pumping), where bot networks trigger high volumes of SMS requests to drain an application's budget.

Why WhatsApp Works for OTP Delivery

WhatsApp is the default communication channel for smartphone users in India. Utilizing WhatsApp for one-time passwords solves several operational issues inherent to SMS:

  1. High presence: Most smartphone users keep WhatsApp open or run it continuously in the background, minimizing delivery latency.
  2. Direct delivery visibility: Unlike SMS gateways that often return ambiguous status codes, message transport across WhatsApp provides reliable delivery state feedback.
  3. Lower abuse vectors: Automated scripts and botnets routinely abuse web forms to trigger bulk SMS over carrier lines. WhatsApp message delivery requires an active, registered account on the destination number, which significantly raises the bar against automated OTP pumping.

The Architecture and Delivery Flow

When implementing OTP verification, you can choose between generating and hashing your own code server-side, or using a dedicated OTP service to handle generation, dispatch, and verification.

If managing generation manually, you must generate a cryptographically secure random token, hash it using a secure algorithm such as SHA-256 before writing to Redis or a database, set a strict TTL, and record attempt counters.

Alternatively, using an OTP API shifts the generation, state tracking, and verification logic upstream. Your backend initiates the challenge with the target phone number, and submits the user's input directly to the provider for validation.

Key engineering controls to maintain in either approach:

  • Limit code validity strictly to 5 minutes (300 seconds).
  • Enforce rate limits at the API gateway layer per phone number (for example, no more than 3 send attempts per 15 minutes) and per client IP address.
  • Enforce a hard ceiling on verification attempts (maximum 5 attempts before the code is permanently invalidated).
  • Never print or write raw OTP codes to system application logs, stdout, or monitoring traces.

Backend Implementation

The examples below illustrate how to integrate a WhatsApp OTP flow using a managed endpoint pattern. The flow requires two steps: dispatching the code via send.php and checking the user-submitted code via verify.php.

PHP Implementation

This helper class uses standard curl to send and verify authentication tokens.

<?php

class WhatsAppAuth {
    private string $apiKey;
    private string $siteUrl;

    public function __construct(string $apiKey, string $siteUrl) {
        $this->apiKey = $apiKey;
        $this->siteUrl = $siteUrl;
    }

    private function post(string $url, array $payload): array {
        $ch = curl_init($url);

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'X-API-Key: ' . $this->apiKey,
                'X-Site-Url: ' . $this->siteUrl
            ],
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_TIMEOUT => 10
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200 || !$response) {
            return ['success' => false, 'error' => 'HTTP request failed'];
        }

        return json_decode($response, true) ?? [];
    }

    public function sendOtp(string $phone, string $purpose = 'login'): array {
        return $this->post('https://app.waloops.com/api/otp/send.php', [
            'phone' => $phone,
            'purpose' => $purpose
        ]);
    }

    public function verifyOtp(string $phone, string $code): bool {
        $result = $this->post('https://app.waloops.com/api/otp/verify.php', [
            'phone' => $phone,
            'code' => $code
        ]);

        return isset($result['verified']) && $result['verified'] === true;
    }
}

// Usage:
$auth = new WhatsAppAuth('your_api_key_here', 'https://yourwebsite.com');

// 1. Send OTP
$sendResult = $auth->sendOtp('919876543210', 'login');
// Returns: ["success" => true, "expires_in" => 300]

// 2. Verify OTP
$isVerified = $auth->verifyOtp('919876543210', '483920');
Enter fullscreen mode Exit fullscreen mode

Node.js Implementation

This Node.js module uses the native fetch API available in modern Node runtimes.

class WhatsAppAuth {
  constructor(apiKey, siteUrl) {
    this.apiKey = apiKey;
    this.siteUrl = siteUrl;
  }

  async #post(url, payload) {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': this.apiKey,
        'X-Site-Url': this.siteUrl
      },
      body: JSON.stringify(payload)
    });

    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }

    return response.json();
  }

  async sendOtp(phone, purpose = 'login') {
    return this.#post('https://app.waloops.com/api/otp/send.php', {
      phone,
      purpose
    });
  }

  async verifyOtp(phone, code) {
    const data = await this.#post('https://app.waloops.com/api/otp/verify.php', {
      phone,
      code
    });
    return Boolean(data.verified);
  }
}

// Usage:
const auth = new WhatsAppAuth('your_api_key_here', 'https://yourwebsite.com');

// 1. Send OTP
const sendResponse = await auth.sendOtp('919876543210', 'login');
// Returns: { success: true, expires_in: 300 }

// 2. Verify OTP
const verified = await auth.verifyOtp('919876543210', '483920');
Enter fullscreen mode Exit fullscreen mode

Security Checklist

Before deploying WhatsApp-based verification to production, ensure your integration includes the following:

  • Enforce strict IP rate limiting on public routes that trigger authentication requests.
  • Validate phone numbers into complete E.164 format (for example, country code without a leading plus sign: 919876543210) before dispatching the payload.
  • Expire codes strictly at 300 seconds (5 minutes) or less.
  • Block repeated verification attempts after 5 incorrect inputs to prevent brute-force attacks.
  • Strip incoming OTP codes from your application server logs and APM tracing tools.

The examples above use the WALoops WhatsApp OTP API which has 100 free OTPs a month. Switching from standard SMS routes to direct WhatsApp messaging bypasses DLT template rejections and delivers codes directly to users without carrier delays.

Top comments (0)