Back to flin
flin

WhatsApp OTP Authentication for Africa

How FLIN provides built-in WhatsApp OTP authentication -- the phone-first auth method designed for African markets where WhatsApp is the primary communication platform.

Thales & Claude | March 25, 2026 8 min flin
flinwhatsappotpafricaauthentication

In Silicon Valley, the default authentication method is email and password, optionally enhanced with Google Sign-In. This assumption fails spectacularly in Africa, where over 300 million people use WhatsApp daily but many do not have a personal email address. A student in Abidjan, a merchant in Lagos, a teacher in Nairobi -- they communicate through WhatsApp, pay through mobile money, and identify themselves by phone number.

Building FLIN without WhatsApp authentication would be like building an American web framework without Google Sign-In. It would technically work, but it would ignore how the target audience actually lives.

FLIN provides WhatsApp OTP as a built-in authentication method. Three functions -- whatsapp_send_otp(), otp_generate(), and the standard verification pattern -- handle the entire flow. No Twilio SDK. No third-party auth service. No per-message billing surprises.

The WhatsApp Authentication Flow

WhatsApp OTP uses a 3-step flow for new users and a 2-step flow for returning users:

New user:
1. Enter phone number -> Send OTP via WhatsApp
2. Enter OTP code -> Verify code
3. Complete profile (name, email, avatar)
4. Create account -> Dashboard

Returning user: 1. Enter phone number -> Send OTP via WhatsApp 2. Enter OTP code -> Verify code -> Dashboard (skip step 3) ```

The key insight is that the phone number is the identity. If the phone is already registered, the user is logged in after OTP verification. If not, they complete a profile form and an account is created.

Step 1: Send OTP

// app/auth/send-whatsapp-otp.flin

layout = "auth"

waPhone = session.waPhone || "" otpSent = false

fn processSendWhatsappOtp() { if waPhone != "" { code = otp_generate(6) result = whatsapp_send_otp(waPhone, code)

if result.success { session.waOtpCode = code session.waOtpPhone = waPhone otpSent = true } } } processSendWhatsappOtp()

{if otpSent}

Enter the code sent to your WhatsApp

We sent a 6-digit code to {waPhone}

{else}

An error occurred. Please try again.

Back to login {/if} ```

The otp_generate(6) function creates a cryptographically random 6-digit code. The whatsapp_send_otp() function sends it via the WhatsApp Business API.

Step 2: Verify OTP

// app/auth/verify-whatsapp-otp.flin

layout = "auth"

otpInput = session.waOtpInput || "" otpCode = session.waOtpCode || "" otpPhone = session.waOtpPhone || ""

// Clear sensitive data immediately session.waOtpInput = none session.waOtpCode = none

verifyOk = false isNewUser = false

fn processVerifyWhatsappOtp() { if otpInput != "" && otpCode != "" && otpPhone != "" { if otpInput == otpCode { existing = User.where(phone == otpPhone && role == "User").first

if existing != none { // Returning user -- log in directly session.user = existing.email session.userName = existing.name || existing.firstName || existing.email session.userId = to_text(existing.id) session.waOtpPhone = none verifyOk = true } else { // New user -- need profile completion session.waVerifiedPhone = otpPhone session.waOtpPhone = none isNewUser = true verifyOk = true } } } } processVerifyWhatsappOtp()

{if verifyOk && !isNewUser}

Welcome back!

{else if verifyOk && isNewUser}

Phone verified! Let's set up your profile.

{else}

Invalid code

The code you entered is incorrect or has expired.

Try again {/if} ```

The critical security pattern: OTP data is cleared from the session immediately after reading. The code exists in session storage for the minimum possible time.

Step 3: Profile Completion (New Users Only)

// app/auth/whatsapp-complete-profile.flin

layout = "auth"

waPhone = session.waVerifiedPhone || "" errorKey = session.waCreateError || "" session.waCreateError = none

{if waPhone == ""} {else}

Complete Your Profile

{if errorKey != ""}

{t(errorKey)}

{/if}

{/if} ```

// app/auth/whatsapp-create-account.flin

route POST { validate { firstName: text @required @minLength(1) email: text @required @email lastName: text occupation: text country: text avatar: file @max_size("5MB") }

waPhone = session.waVerifiedPhone || "" if waPhone == "" { redirect("/login") }

// Check email uniqueness existingEmail = User.where(email == body.email && role == "User").first if existingEmail != none { session.waCreateError = "error.email_taken" redirect("/auth/whatsapp-complete-profile") }

// Check phone uniqueness existingPhone = User.where(phone == waPhone && role == "User").first if existingPhone != none { session.user = existingPhone.email session.userName = existingPhone.name session.userId = to_text(existingPhone.id) session.waVerifiedPhone = none redirect("/tasks") }

avatarPath = "" if body.avatar != none { avatarPath = save_file(body.avatar, ".flindb/avatars/") }

fullName = to_text(body.firstName) + " " + to_text(body.lastName || "") newUser = User { email: body.email, name: fullName, firstName: body.firstName, lastName: body.lastName || "", phone: waPhone, provider: "WhatsApp", avatar: avatarPath, occupation: body.occupation || "", country: body.country || "", emailVerified: false } save newUser

session.waVerifiedPhone = none session.user = newUser.email session.userName = fullName session.userId = to_text(newUser.id)

redirect("/tasks") } ```

The whatsapp_send_otp() Function

The built-in function handles the WhatsApp Business API integration:

result = whatsapp_send_otp(phone_number, code)
// result.success -> true/false
// result.error -> error message if failed

Under the hood, FLIN sends the OTP through the WhatsApp Business API using a pre-approved message template. The template is configured once in the WhatsApp Business Manager and referenced by the runtime:

pub async fn send_whatsapp_otp(
    phone: &str,
    code: &str,
) -> Result<SendResult, WhatsAppError> {
    let api_url = env::var("WHATSAPP_API_URL")?;
    let token = env::var("WHATSAPP_TOKEN")?;
    let template_name = env::var("WHATSAPP_OTP_TEMPLATE")
        .unwrap_or_else(|_| "otp_verification".to_string());

let payload = json!({ "messaging_product": "whatsapp", "to": normalize_phone(phone), "type": "template", "template": { "name": template_name, "language": { "code": "en" }, "components": [{ "type": "body", "parameters": [{ "type": "text", "text": code }] }] } });

let response = reqwest::Client::new() .post(&api_url) .bearer_auth(&token) .json(&payload) .send() .await?;

if response.status().is_success() { Ok(SendResult { success: true, error: None }) } else { let error = response.text().await.unwrap_or_default(); Ok(SendResult { success: false, error: Some(error) }) } } ```

Phone Number Normalization

African phone numbers come in many formats: +225 07 08 09 10, 00225 0708091010, 07 08 09 10, 225708091010. FLIN normalizes all of these to E.164 format (+2250708091010) before sending:

fn normalize_phone(phone: &str) -> String {
    // Remove spaces, dashes, parentheses
    let digits: String = phone.chars()
        .filter(|c| c.is_ascii_digit() || *c == '+')
        .collect();

if digits.starts_with('+') { digits } else if digits.starts_with("00") { format!("+{}", &digits[2..]) } else if digits.len() == 10 { // Assume local format -- need country code from config let country_code = env::var("DEFAULT_PHONE_COUNTRY").unwrap_or("225".into()); format!("+{}{}", country_code, digits) } else { format!("+{}", digits) } } ```

Why WhatsApp OTP for Africa

The decision to build WhatsApp OTP into FLIN was driven by market reality:

WhatsApp penetration. In Cote d'Ivoire, Nigeria, Kenya, Ghana, South Africa, and most of Sub-Saharan Africa, WhatsApp is the default messaging platform. Most people check WhatsApp before they check email.

Email scarcity. Many African internet users, especially outside major cities, do not have a personal email address. Requiring email registration excludes them.

Phone-first identity. Mobile money (MTN Mobile Money, Orange Money, Wave, M-Pesa) uses phone numbers as identifiers. Government services increasingly accept phone-based verification. A phone number is the most universal form of digital identity in Africa.

SMS costs. SMS-based OTP is expensive in Africa (0.03-0.10 USD per message) and unreliable across carriers. WhatsApp messages cost a fraction of that through the Business API and are delivered reliably over data connections.

Trust. Users trust messages from WhatsApp. A verification code received in WhatsApp feels legitimate. A code received via SMS might look like spam.

By making WhatsApp OTP a built-in feature, FLIN positions itself as a framework that understands its primary market. A developer in Abidjan building an app for West African users can add phone-based authentication in minutes, not days.

Security Considerations

WhatsApp OTP has specific security considerations:

Code expiration. FLIN's OTP codes are valid for 10 minutes. After that, the session data is invalidated and a new code must be requested.

Rate limiting. The send-OTP endpoint should be rate-limited to prevent abuse. The guard rate_limit(3, 300) guard (3 requests per 5 minutes) is recommended for OTP endpoints.

Code length. The 6-digit code provides 1 million possible combinations. Combined with rate limiting (5 attempts per session), brute force is impractical.

Session hygiene. OTP data is cleared from the session immediately after use. The code, the phone number, and the verification input are never stored longer than necessary.

FLIN's WhatsApp OTP is not a wrapper around a third-party auth service. It is a first-class authentication method built into the language, with the same session management, validation, and security guarantees as email/password authentication.

In the next article, we cover request body validators -- how FLIN's validate blocks enforce type safety, constraints, and business rules on incoming data before your handler code runs.

---

This is Part 112 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.

Series Navigation: - [111] OAuth2 and Social Authentication - [112] WhatsApp OTP Authentication for Africa (you are here) - [113] Request Body Validators - [114] 75 Security Tests: How We Verified Everything

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles