Skip to main content
In Nigeria and most of Africa, WhatsApp is how people actually communicate — but delivery is less predictable than SMS, and some users have data off. The practical pattern: WhatsApp primary, SMS as automatic fallback.
WhatsApp Business API sending isn’t part of Robase’s core API yet — it’s on the Phase 4 roadmap. This guide shows the pattern using your own WhatsApp provider today (Twilio, 360dialog, Infobip), falling back to pm.sms.send when WhatsApp fails.

Why fallback, not one or the other

ChannelStrengthsWeaknesses
WhatsAppRich formatting, receipts, delivery fast when online, no segment costRequires data on; template approval; higher per-msg cost
SMSWorks offline, no template approval, cheaperPlain text only, 160 chars, no read receipts
Send via WhatsApp first, watch for delivery, and fall back to SMS if it doesn’t deliver within a window.

The flow

 App event  →  WhatsApp send

         Webhook: delivered / read?    (wait up to 60 s)

          Yes                 No / timeout

                              SMS send

Reference implementation

import { Robase } from '@robase/node';
import WhatsApp from 'my-whatsapp-sdk';
import Bull from 'bull';

const pm = new Robase(process.env.ROBASE_API_KEY!);
const wa = new WhatsApp(process.env.WA_TOKEN);
const fallbackQueue = new Bull('wa-fallback');

async function notify(userId: string, body: { short: string; rich: string }) {
  const user = await db.users.get(userId);
  let wa_message_id: string | null = null;

  // 1. Try WhatsApp first.
  if (user.whatsapp_opted_in) {
    try {
      const { id } = await wa.send({ to: user.phone, body: body.rich });
      wa_message_id = id;
    } catch (e) {
      console.warn('wa send failed, fallback immediate', e);
      return smsFallback(user, body.short);
    }
  } else {
    return smsFallback(user, body.short);
  }

  // 2. Schedule the SMS fallback for 60s later — cancel if WhatsApp delivers.
  await fallbackQueue.add(
    { user_id: userId, message: body.short, wa_message_id },
    { delay: 60_000, jobId: `fallback-${wa_message_id}` }
  );
}

async function smsFallback(user: { phone: string }, body: string) {
  await pm.sms.send(
    { to: user.phone, from: 'SHUTTLERS', body },
    `fallback-${user.phone}-${Date.now()}`,
  );
}

// WhatsApp webhook: cancel the fallback when delivered.
app.post('/webhooks/whatsapp', async (req, res) => {
  if (!verifyWhatsApp(req)) return res.status(401).end();
  const event = req.body;
  if (event.type === 'message.delivered') {
    await fallbackQueue.removeJobs(`fallback-${event.message_id}`);
  }
  res.status(200).end();
});

// Fallback worker.
fallbackQueue.process(async (job) => {
  const { user_id, message } = job.data;
  const user = await db.users.get(user_id);
  await pm.sms.send({ to: user.phone, from: 'SHUTTLERS', body: message }, `fb-${job.id}`);
});

Tuning the fallback window

  • OTP: 30–45s. Users expect the code fast.
  • Ride ETA: 60s.
  • Receipts / non-urgent: 5 min. Most users read WhatsApp within 5 minutes; SMS is a safety net, not a duplication.
  • Marketing: Don’t fall back. If WhatsApp fails, the message isn’t worth $1 of SMS.

Two versions of the body

Keep your WhatsApp body rich (links, bold, emoji) and the SMS body tight (≤ 160 GSM-7):
await notify(user.id, {
  rich:  `*Your ride is on its way* 🚗\n\nDriver: ${driver.name}\nPlate: ${vehicle.plate}\n\nTrack: https://shut.ng/t/${trip.id}`,
  short: `Your ride is here. Driver: ${driver.name}. Plate: ${vehicle.plate}. Track: shut.ng/t/${trip.id}`,
});

Measuring the fallback rate

High fallback rate (say, >30%) usually means:
  1. Users disabled WhatsApp notifications — they’ll still receive the SMS, but your WhatsApp cost is wasted. Check opt-in quality.
  2. WhatsApp templates not approved — silent delivery failures. Check your WA dashboard.
  3. Network issues in a specific region — cluster by user location; route problem regions to SMS-primary.
Track both channels side by side; rolling 7-day fallback rate is the single most informative metric.