DEV Community

Cover image for Email OTP Is Broken in Morocco (and Most of MENA) — Here's What Actually Works
SOUFIAN SEJJARI
SOUFIAN SEJJARI

Posted on • Originally published at wasel.ma

Email OTP Is Broken in Morocco (and Most of MENA) — Here's What Actually Works

You've just launched your app. Sign-up flow looks clean. You fire the OTP to the user's email.

They never see it.

Not in spam. Not in promotions. Just... gone. Or received 4 minutes later, after the code expired.

In Europe or the US, this is a minor UX annoyance. In Morocco and most of MENA, it's a conversion killer.

Here's why — and what you can do about it today.

Why email fails in Morocco

Morocco has ~95% WhatsApp penetration on smartphones. Every shop owner, every patient, every customer manages their life through WhatsApp. It's open all day. It has notifications. It gets read.

Email? Most users don't have a mail app configured on their phone. And those who do have trained themselves to ignore it — promotional tabs, spam filters, delivery delays. The few that get through land 4 hours later when the OTP has long expired.

This isn't a Moroccan quirk. It's true across MENA, Sub-Saharan Africa, and large parts of Southeast Asia. WhatsApp is the inbox. Email is where you store receipts you'll never open.

WhatsApp vs SMS vs Email: honest comparison for Morocco

Email SMS WhatsApp
Open rate (Morocco) 8–14% ~75% 90–98%
Delivery speed 0–10 min Instant Instant
Read time Hours / days Minutes Seconds
Cost per message ~$0.001 ~$0.04–0.07 ~$0.005–0.015
User friction High (app switch) Low Very low
Copy code UX No No ✅ Native button
Rich content HTML (often clipped) Plain text Templates + media
Works offline No No
Moroccan user expectation Low Medium High

Honest take: SMS is still better than email in Morocco and works offline. But WhatsApp costs less than SMS, has better UX, and users are already there. The only real case for SMS is when you need guaranteed offline delivery.

What breaks in your funnel right now

User signs up
  → OTP sent to email
  → User doesn't have push email configured (very common)
  → OTP expires after 5 minutes
  → User retries 2–3x
  → User abandons
Enter fullscreen mode Exit fullscreen mode

Your funnel leaks at peak intent. Your email provider reports it as "delivered." You have zero visibility.

Real numbers from Moroccan deployments after switching to WhatsApp OTP:

  • Sign-up completion rate: +35–50%
  • "I didn't receive the code" support tickets: -70–80%
  • Time to verified: ~4 min average → ~25 seconds

The same logic applies to every transactional notification — not just OTP. Here's what WhatsApp messages actually look like in practice:

Order confirmation (e-commerce)

✅ Votre commande #4182 est confirmée.
Livraison estimée : demain entre 14h–18h.
👉 Suivre ma commande
Enter fullscreen mode Exit fullscreen mode

Appointment reminder (clinic / salon)

Bonjour Mme. Benali 👋
Rappel : RDV demain à 10h30 chez Dr. Alaoui.
Répondre OUI pour confirmer, NON pour annuler.
Enter fullscreen mode Exit fullscreen mode

Shipping update (retail)

📦 Votre colis est en route !
Numéro de suivi : AM-7712
Livraison prévue : aujourd'hui
Enter fullscreen mode Exit fullscreen mode

Each of these gets ~12% open rate via email. Via WhatsApp: users respond within minutes — and you can build two-way flows where the reply actually triggers a workflow.

The API: drop-in WhatsApp OTP for any backend

Wasel exposes a REST API that lets any CRM, ERP, or backend send WhatsApp OTPs and template messages — without touching the WhatsApp Business API directly.

Base URL: https://wasel-api.wasel.ma/external/v1
Auth: X-API-Key: ext_YOUR_API_KEY_HERE

The cleanest part of the OTP flow: you never handle the code yourself. The API generates it, sends it, and validates it. Your backend calls two endpoints.

Step 1 — Send

curl -s -X POST "https://wasel-api.wasel.ma/external/v1/otp/send" \
  -H "X-API-Key: ext_YOUR_API_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+212600000000",
    "lang": "fr",
    "ttl_minutes": 10,
    "reference": "session_abc123"
  }' | jq
Enter fullscreen mode Exit fullscreen mode

lang supports fr / ar / en. reference is your session ID — it binds the send to the verify call so two concurrent flows don't collide.

The user gets a native WhatsApp message with a one-tap "Copy code" button. No typing, no app-switching.

Step 2 — Verify

curl -s -X POST "https://wasel-api.wasel.ma/external/v1/otp/verify" \
  -H "X-API-Key: ext_YOUR_API_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+212600000000",
    "code": "847291",
    "reference": "session_abc123"
  }' | jq
Enter fullscreen mode Exit fullscreen mode

Response reason values — handle all of these:

reason meaning
verified ✅ Correct code
invalid_code Wrong — attempts counter incremented
expired TTL passed — re-send required
max_attempts_exceeded Too many wrong guesses
not_found No pending OTP for this phone

Check status anytime

curl -s "https://wasel-api.wasel.ma/external/v1/otp/status?phone=%2B212600000000&reference=session_abc123" \
  -H "X-API-Key: ext_YOUR_API_KEY_HERE" | jq
Enter fullscreen mode Exit fullscreen mode

Beyond OTP: transactional notifications from your CRM/ERP

Same API key, same pattern. Send a single template:

curl -X POST "https://wasel-api.wasel.ma/external/v1/send-template" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ext_YOUR_API_KEY_HERE" \
  -d '{
    "phone": "+212600000000",
    "template_name": "order_confirmation",
    "lang": "fr",
    "variables": ["ORD-1001"],
    "response_action_key": "order-confirm-v1",
    "custom_data": {
      "orderNumber": "ORD-1001",
      "source": "erp"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Two fields worth knowing:

  • response_action_key — binds the user's WhatsApp reply to a workflow (user replies "Confirmer" → fires an automation)
  • custom_data — your arbitrary JSON, stored with the message for downstream ERP handling

Or bulk up to 500 recipients, personalized per line:

curl -X POST "https://wasel-api.wasel.ma/external/v1/send-template-bulk" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ext_YOUR_API_KEY_HERE" \
  -d '{
    "template_name": "appointment_reminder",
    "lang": "fr",
    "recipients": [
      { "phone": "+212600000001", "variables": ["demain à 10h"] },
      { "phone": "+212600000002", "variables": ["demain à 14h"] }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

Error handling

400  invalid payload / missing fields / template not found
401  invalid or missing X-API-Key
429  rate limited — back off and retry
502  WhatsApp delivery failed — retry after a few seconds
Enter fullscreen mode Exit fullscreen mode

Rate limit: 60 req/min per key. A simple retry handles the transient cases:

async function sendWithRetry(payload, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fetch('https://wasel-api.wasel.ma/external/v1/send-template', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.WASEL_API_KEY
      },
      body: JSON.stringify(payload)
    });

    if (res.status === 429 || res.status === 502) {
      await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
      continue;
    }

    return res.json();
  }
  throw new Error('Max retries exceeded');
}
Enter fullscreen mode Exit fullscreen mode

Compliance

WhatsApp requires opt-in. For OTP and transactional messages: the user providing their phone number is sufficient consent. For marketing messages: explicit checkbox required. Morocco follows CNDP regulations, broadly aligned with GDPR.


TL;DR

  1. Email OTP in Morocco has 8–14% open rate. WhatsApp: 90%+. That gap is your funnel leak.
  2. /otp/send + /otp/verify — you never touch the code itself, just call two endpoints.
  3. Same API key covers all transactional notifications your stack needs.
  4. response_action_key wires user replies back into your existing workflows.
  5. Standard REST, X-API-Key auth, 60 req/min.

If you're building for Moroccan or MENA users and still routing critical messages through email: you're not delivering them.


🚀 Try Wasel free for 7 days — no credit card required


Built something with the Wasel API, or have different numbers from your market? Drop a comment — happy to compare notes.

Top comments (0)