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
| Channel | Strengths | Weaknesses |
|---|
| WhatsApp | Rich formatting, receipts, delivery fast when online, no segment cost | Requires data on; template approval; higher per-msg cost |
| SMS | Works offline, no template approval, cheaper | Plain 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:
- Users disabled WhatsApp notifications — they’ll still receive the SMS, but your WhatsApp cost is wasted. Check opt-in quality.
- WhatsApp templates not approved — silent delivery failures. Check your WA dashboard.
- 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.