<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SOUFIAN SEJJARI</title>
    <description>The latest articles on DEV Community by SOUFIAN SEJJARI (@soufian_sejjari).</description>
    <link>https://dev.to/soufian_sejjari</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3837214%2Fb39b9242-738e-4931-b62b-02d0f4a97707.jpg</url>
      <title>DEV Community: SOUFIAN SEJJARI</title>
      <link>https://dev.to/soufian_sejjari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/soufian_sejjari"/>
    <language>en</language>
    <item>
      <title>Email OTP Is Broken in Morocco (and Most of MENA) — Here's What Actually Works</title>
      <dc:creator>SOUFIAN SEJJARI</dc:creator>
      <pubDate>Mon, 08 Jun 2026 08:17:40 +0000</pubDate>
      <link>https://dev.to/soufian_sejjari/email-otp-is-broken-in-morocco-and-most-of-mena-heres-what-actually-works-5e0</link>
      <guid>https://dev.to/soufian_sejjari/email-otp-is-broken-in-morocco-and-most-of-mena-heres-what-actually-works-5e0</guid>
      <description>&lt;p&gt;You've just launched your app. Sign-up flow looks clean. You fire the OTP to the user's email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They never see it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not in spam. Not in promotions. Just... gone. Or received 4 minutes later, after the code expired.&lt;/p&gt;

&lt;p&gt;In Europe or the US, this is a minor UX annoyance. In Morocco and most of MENA, &lt;strong&gt;it's a conversion killer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's why — and what you can do about it today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email fails in Morocco
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  WhatsApp vs SMS vs Email: honest comparison for Morocco
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;th&gt;SMS&lt;/th&gt;
&lt;th&gt;WhatsApp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open rate (Morocco)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8–14%&lt;/td&gt;
&lt;td&gt;~75%&lt;/td&gt;
&lt;td&gt;90–98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delivery speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0–10 min&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hours / days&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost per message&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$0.001&lt;/td&gt;
&lt;td&gt;~$0.04–0.07&lt;/td&gt;
&lt;td&gt;~$0.005–0.015&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User friction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (app switch)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Copy code UX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;✅ Native button&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rich content&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTML (often clipped)&lt;/td&gt;
&lt;td&gt;Plain text&lt;/td&gt;
&lt;td&gt;Templates + media&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works offline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Moroccan user expectation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Honest take:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks in your funnel right now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your funnel leaks at peak intent. Your email provider reports it as "delivered." You have zero visibility.&lt;/p&gt;

&lt;p&gt;Real numbers from Moroccan deployments after switching to WhatsApp OTP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sign-up completion rate:&lt;/strong&gt; +35–50%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"I didn't receive the code" support tickets:&lt;/strong&gt; -70–80%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to verified:&lt;/strong&gt; ~4 min average → ~25 seconds&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Order confirmation (e-commerce)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Votre commande #4182 est confirmée.
Livraison estimée : demain entre 14h–18h.
👉 Suivre ma commande
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Appointment reminder (clinic / salon)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bonjour Mme. Benali 👋
Rappel : RDV demain à 10h30 chez Dr. Alaoui.
Répondre OUI pour confirmer, NON pour annuler.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Shipping update (retail)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📦 Votre colis est en route !
Numéro de suivi : AM-7712
Livraison prévue : aujourd'hui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API: drop-in WhatsApp OTP for any backend
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://wasel.ma" rel="noopener noreferrer"&gt;Wasel&lt;/a&gt; exposes a REST API that lets any CRM, ERP, or backend send WhatsApp OTPs and template messages — without touching the WhatsApp Business API directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;https://wasel-api.wasel.ma/external/v1&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Auth:&lt;/strong&gt; &lt;code&gt;X-API-Key: ext_YOUR_API_KEY_HERE&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The cleanest part of the OTP flow: &lt;strong&gt;you never handle the code yourself&lt;/strong&gt;. The API generates it, sends it, and validates it. Your backend calls two endpoints.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1 — Send
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://wasel-api.wasel.ma/external/v1/otp/send"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: ext_YOUR_API_KEY_HERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "phone": "+212600000000",
    "lang": "fr",
    "ttl_minutes": 10,
    "reference": "session_abc123"
  }'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

&lt;p&gt;The user gets a native WhatsApp message with a &lt;strong&gt;one-tap "Copy code" button&lt;/strong&gt;. No typing, no app-switching.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2 — Verify
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://wasel-api.wasel.ma/external/v1/otp/verify"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: ext_YOUR_API_KEY_HERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "phone": "+212600000000",
    "code": "847291",
    "reference": "session_abc123"
  }'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Response &lt;code&gt;reason&lt;/code&gt; values — handle all of these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;reason&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verified&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Correct code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invalid_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wrong — attempts counter incremented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TTL passed — re-send required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max_attempts_exceeded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Too many wrong guesses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not_found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No pending OTP for this phone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Check status anytime
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://wasel-api.wasel.ma/external/v1/otp/status?phone=%2B212600000000&amp;amp;reference=session_abc123"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: ext_YOUR_API_KEY_HERE"&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Beyond OTP: transactional notifications from your CRM/ERP
&lt;/h2&gt;

&lt;p&gt;Same API key, same pattern. Send a single template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://wasel-api.wasel.ma/external/v1/send-template"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: ext_YOUR_API_KEY_HERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "phone": "+212600000000",
    "template_name": "order_confirmation",
    "lang": "fr",
    "variables": ["ORD-1001"],
    "response_action_key": "order-confirm-v1",
    "custom_data": {
      "orderNumber": "ORD-1001",
      "source": "erp"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two fields worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;response_action_key&lt;/code&gt; — binds the user's WhatsApp reply to a workflow (user replies "Confirmer" → fires an automation)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;custom_data&lt;/code&gt; — your arbitrary JSON, stored with the message for downstream ERP handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or bulk up to 500 recipients, personalized per line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://wasel-api.wasel.ma/external/v1/send-template-bulk"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: ext_YOUR_API_KEY_HERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "template_name": "appointment_reminder",
    "lang": "fr",
    "recipients": [
      { "phone": "+212600000001", "variables": ["demain à 10h"] },
      { "phone": "+212600000002", "variables": ["demain à 14h"] }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Error handling
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;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
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rate limit: &lt;strong&gt;60 req/min per key&lt;/strong&gt;. A simple retry handles the transient cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://wasel-api.wasel.ma/external/v1/send-template&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WASEL_API_KEY&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Max retries exceeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Compliance
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

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

&lt;p&gt;If you're building for Moroccan or MENA users and still routing critical messages through email: you're not delivering them.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://wasel.ma" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;🚀 Try Wasel free for 7 days — no credit card required&lt;/a&gt;
&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built something with the Wasel API, or have different numbers from your market? Drop a comment — happy to compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>api</category>
      <category>webdev</category>
      <category>morocco</category>
    </item>
    <item>
      <title>Claude Code Channels Is the Feature That Might Make You Rethink OpenCode</title>
      <dc:creator>SOUFIAN SEJJARI</dc:creator>
      <pubDate>Sat, 21 Mar 2026 16:13:24 +0000</pubDate>
      <link>https://dev.to/soufian_sejjari/claude-code-channels-is-the-feature-that-might-make-you-rethink-opencode-3c7p</link>
      <guid>https://dev.to/soufian_sejjari/claude-code-channels-is-the-feature-that-might-make-you-rethink-opencode-3c7p</guid>
      <description>&lt;p&gt;When Claude Code Channels dropped, it didn’t look like a big deal.&lt;/p&gt;

&lt;p&gt;Just another feature in the changelog.&lt;/p&gt;

&lt;p&gt;But if you look closely, it actually changes something fundamental —&lt;br&gt;
not just for Claude Code, but for how we think about AI coding tools in general.&lt;/p&gt;

&lt;p&gt;👉 Original article: &lt;a href="https://opuslon.com/en/blog/claude-code-channels-vs-opencode" rel="noopener noreferrer"&gt;https://opuslon.com/en/blog/claude-code-channels-vs-opencode&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  We’re moving beyond “AI in the terminal”
&lt;/h2&gt;

&lt;p&gt;Most coding agents today follow the same pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You open a session&lt;/li&gt;
&lt;li&gt;You write a prompt&lt;/li&gt;
&lt;li&gt;You get a response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even the best tools are still &lt;strong&gt;reactive&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They wait.&lt;/p&gt;

&lt;p&gt;They don’t &lt;em&gt;live&lt;/em&gt; inside your workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude Code Channels changes
&lt;/h2&gt;

&lt;p&gt;Channels introduces a different model.&lt;/p&gt;

&lt;p&gt;Instead of waiting for input, Claude Code can now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;receive &lt;strong&gt;external events&lt;/strong&gt; (CI, webhooks, alerts)&lt;/li&gt;
&lt;li&gt;stay connected to your &lt;strong&gt;local environment&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;continue working while you’re away&lt;/li&gt;
&lt;li&gt;ask for approval only when necessary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns it into something closer to a &lt;strong&gt;persistent, event-driven system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not just an assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3 capabilities that matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. External events → live session
&lt;/h3&gt;

&lt;p&gt;Instead of starting a new task or polling:&lt;/p&gt;

&lt;p&gt;Events are injected directly into your running session.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI pipeline fails&lt;/li&gt;
&lt;li&gt;deployment breaks&lt;/li&gt;
&lt;li&gt;monitoring alert triggers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude receives that inside the same context where your code already exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Two-way communication
&lt;/h3&gt;

&lt;p&gt;Channels can connect your session to tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegram&lt;/li&gt;
&lt;li&gt;Discord&lt;/li&gt;
&lt;li&gt;custom interfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meaning you can interact with your local environment remotely — without switching context.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Permission relay (this is the real unlock)
&lt;/h3&gt;

&lt;p&gt;Claude can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pause on sensitive actions&lt;/li&gt;
&lt;li&gt;send approval requests to your phone&lt;/li&gt;
&lt;li&gt;continue once approved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what makes the system usable in real workflows.&lt;/p&gt;

&lt;p&gt;Without it, everything blocks.&lt;/p&gt;

&lt;p&gt;With it, you get &lt;strong&gt;controlled autonomy&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this changes the Claude Code vs OpenCode debate
&lt;/h2&gt;

&lt;p&gt;OpenCode is still very strong.&lt;/p&gt;

&lt;p&gt;It offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open-source flexibility&lt;/li&gt;
&lt;li&gt;multi-provider support&lt;/li&gt;
&lt;li&gt;strong portability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those are your priorities, OpenCode makes total sense.&lt;/p&gt;

&lt;p&gt;But Channels introduces something different:&lt;/p&gt;

&lt;p&gt;👉 a &lt;strong&gt;native event-driven workflow layer inside a local session&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s not just a feature — it’s a different direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  A new way to compare these tools
&lt;/h2&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which one is better?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It becomes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose OpenCode if you want:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;openness&lt;/li&gt;
&lt;li&gt;provider control&lt;/li&gt;
&lt;li&gt;flexibility across models&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Choose Claude Code if you want:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;persistent local context&lt;/li&gt;
&lt;li&gt;event-driven workflows&lt;/li&gt;
&lt;li&gt;real-world integration (CI, alerts, chat)&lt;/li&gt;
&lt;li&gt;human-in-the-loop automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-world use cases
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI &amp;amp; deployment
&lt;/h3&gt;

&lt;p&gt;A build fails → Claude receives the event → starts debugging immediately in your local repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  On-call workflows
&lt;/h3&gt;

&lt;p&gt;Alerts from Sentry or infra tools trigger analysis before you even open your laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chat-driven interaction
&lt;/h3&gt;

&lt;p&gt;You message your system → Claude responds using your real local codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controlled automation
&lt;/h3&gt;

&lt;p&gt;Claude progresses, pauses for approval, then continues.&lt;/p&gt;

&lt;p&gt;Not fully autonomous. Not fully manual.&lt;/p&gt;

&lt;p&gt;Something in between — and much more usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important limitations (for now)
&lt;/h2&gt;

&lt;p&gt;Let’s stay realistic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;still in &lt;strong&gt;research preview&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;requires latest Claude Code versions&lt;/li&gt;
&lt;li&gt;session must stay running&lt;/li&gt;
&lt;li&gt;limited plugin ecosystem&lt;/li&gt;
&lt;li&gt;security needs to be handled carefully&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a finished platform.&lt;/p&gt;

&lt;p&gt;But it’s a clear direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Claude Code Channels doesn’t kill OpenCode.&lt;/p&gt;

&lt;p&gt;But it forces a new question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do you want an assistant you prompt,&lt;br&gt;
or a system that stays connected to your environment?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s where things are heading.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 Full breakdown
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://opuslon.com/en/blog/claude-code-channels-vs-opencode" rel="noopener noreferrer"&gt;https://opuslon.com/en/blog/claude-code-channels-vs-opencode&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>openclaw</category>
    </item>
  </channel>
</rss>
