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
| SMS | |||
|---|---|---|---|
| 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
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
Appointment reminder (clinic / salon)
Bonjour Mme. Benali 👋
Rappel : RDV demain à 10h30 chez Dr. Alaoui.
Répondre OUI pour confirmer, NON pour annuler.
Shipping update (retail)
📦 Votre colis est en route !
Numéro de suivi : AM-7712
Livraison prévue : aujourd'hui
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
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
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
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"
}
}'
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"] }
]
}'
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
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');
}
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
- Email OTP in Morocco has 8–14% open rate. WhatsApp: 90%+. That gap is your funnel leak.
-
/otp/send+/otp/verify— you never touch the code itself, just call two endpoints. - Same API key covers all transactional notifications your stack needs.
-
response_action_keywires user replies back into your existing workflows. - Standard REST,
X-API-Keyauth, 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)