What businesses are
building with SMS
Step-by-step integration recipes for the four most common SMS use cases — with real code you can copy and ship today.
OTP & 2FA
Send one-time passwords for account login, payments, and sensitive actions.
Payment Alerts
Real-time SMS alerts for mobile money receipts, bank deposits, and withdrawals.
Order Tracking
Automated updates when an order is received, processed, out for delivery, or completed.
Marketing Blasts
Run SMS campaigns for seasonal offers, flash sales, and personalized customer promos.
How it works
OTP & Two-Factor Authentication
Send one-time passwords for account login, payments, and sensitive actions. Follow these steps to build a secure, production-ready OTP flow.
Generate a secure OTP server-side
Always generate OTPs on your backend — never on the client. Use a cryptographically random source, store a hashed copy with an expiry timestamp.
Send OTP via MojaWave SMS API
Call POST /v1/sms with your TCRA Sender ID and the generated code. Include an expiry hint in the message.
Verify the code submitted by the user
Compare hashes, check the expiry, enforce a max-attempts counter. On success, invalidate the OTP so it can't be reused.
Handle delivery receipt via webhook
Subscribe to MojaWave delivery webhooks so you know if the OTP SMS was actually delivered. Offer a resend option after 60 seconds if undelivered.
// Step 1: Generate & store OTP (Node.js) const crypto = require('crypto'); const bcrypt = require('bcrypt'); async function createOTP(phone) { const otp = crypto.randomInt( 100000, 999999 ).toString(); const hash = await bcrypt.hash(otp, 10); const expiresAt = Date.now() + 5*60*1000; await db.otps.upsert({ phone, hash, expiresAt, attempts: 0 }); return otp; // send via SMS only }
import secrets, bcrypt from datetime import datetime, timedelta def create_otp(phone: str) -> str: otp = str(secrets.randbelow(900000) + 100000) hashed = bcrypt.hashpw( otp.encode(), bcrypt.gensalt() ) expires = (datetime.utcnow() + timedelta(minutes=5)) db.otps.upsert({ "phone": phone, "hash": hashed, "expires_at": expires, "attempts": 0 }) return otp
# Step 2: Send OTP via MojaWave curl -X POST \ https://api.mojawave.com/v1/sms \ -H "Authorization: Bearer sk_live_mw_xxx" \ -H "Content-Type: application/json" \ -d '{ "to": "+255755123456", "from": "Duka Masta", "message": "Your code: 482139. 5 min." }' // 200 OK { "id": "sms_7k2mx9", "status": "queued", "network": "vodacom", "cost_tzs": 20 }
📱 SMS Preview
Duka Masta
Just now · Delivered ✓✓
Hash, never plaintext
Store bcrypt/argon2 hashes. If your DB leaks, OTPs are useless to attackers.
5-minute expiry
Longer widens the attack window. Shorter frustrates users. 5 min is the sweet spot.
Rate-limit resends
Allow resend only after 60 s and max 3 per session to prevent SMS flooding.
Log delivery receipts
If the OTP SMS fails to deliver, don't silently fail — show the user an error.
How it works
Payment Alerts
Send instant confirmation SMS immediately after a Mobile Money transaction or card payment is processed.
Listen to your payment provider webhook
Subscribe to transaction events from M-Pesa, Selcom, or your payment gateway. Validate the signature before processing.
Compose a clear, concise alert message
Include: amount, transaction reference, sender name, and new balance. Keep under 160 chars to stay in one SMS segment.
Send via MojaWave and log the SMS ID
Fire the SMS call immediately on payment success. Store the returned sms_id alongside the transaction record.
Retry on delivery failure
If the delivery webhook shows failed, retry once after 30 s then mark as undeliverable in your records.
// Express webhook handler app.post('/webhooks/payment', async (req, res) => { const { event, transaction } = req.body; if (event !== 'payment.success') return res.sendStatus(200); const { amount, phone, ref, balance } = transaction; const message = `Duka Masta: Umepokea TZS ` + `${amount.toLocaleString()} ` + `kutoka ${ref}. ` + `Salio: TZS ${balance.toLocaleString()}.`; const sms = await mojawave.sms.send({ to: phone, from: 'Duka Masta', message }); await db.transactions.update(ref, { smsId: sms.id }); res.sendStatus(200); });
📱 SMS Preview
Duka Masta
Just now · Delivered ✓✓
Fire-and-don't-await
Return HTTP 200 to your payment provider immediately. Send SMS async via a queue.
Use Swahili for trust
Swahili messages feel local and trusted. Include your merchant name to reduce fraud confusion.
Always include a ref
A short transaction ref lets customers call support with context — critical for dispute resolution.
Keep under 160 chars
161+ chars splits into 2 segments and doubles your cost. Use a char counter in your template builder.
How it works
Order Status Updates
Keep customers informed at every stage: confirmed, shipped, out for delivery, and delivered.
Define order status event triggers
Map your order states to SMS triggers: CONFIRMED, SHIPPED, OUT_FOR_DELIVERY, DELIVERED.
Build personalised message templates
Include the customer's first name, order reference, and relevant ETA. Short, clear messages outperform verbose ones.
Send SMS at each status transition
Hook into your OMS state-change events and fire a corresponding SMS. Use idempotency keys to prevent duplicate sends.
Track delivery and log per order
Store each sms_id against the order so support staff can check delivery status if a customer claims they never got an update.
const TEMPLATES = { CONFIRMED: (o) => `Habari ${o.name}! Oda #${o.ref} ` + `imekubaliwa.`, SHIPPED: (o) => `Oda #${o.ref} imetumwa! ` + `ETA: ${o.eta}. Track: duka.co/t/${o.ref}`, OUT_FOR_DELIVERY: (o) => `${o.name}, oda #${o.ref} ipo njiani! ` + `Itafika leo. Kuwa karibu.`, DELIVERED: (o) => `✓ Oda #${o.ref} imefikia. Asante! ` + `Tathmini: duka.co/review/${o.ref}` }; async function notifyStatus(order, status) { return mojawave.sms.send({ to: order.phone, from: 'Duka Masta', message: TEMPLATES[status](order), metadata: { orderId: order.ref, status } }); }
📱 SMS Thread Preview
Duka Masta
12:05 ✓✓
14:22 ✓✓
16:40 ✓✓
Use customer's first name
Personalisation dramatically improves perceived quality. "Habari Amina!" vs "Dear Customer".
Idempotency prevents duplicates
Pass orderId + status as idempotency key so a retry bug doesn't double-send.
Include a tracking link
Even a simple order lookup page reduces WISMO support calls by up to 40%.
Respect quiet hours
Don't send between 10pm–7am unless same-day delivery. Use the send_at parameter.
How it works
Marketing Blasts
Send promotions, reminders, and re-engagement campaigns to opted-in customers — with TCRA-compliant opt-out handling built in.
Build your opted-in recipient list
Only send to customers who explicitly opted in. Filter out any phone numbers that have replied STOP. This is both a legal requirement and the ethical standard.
Compose your campaign message
Lead with your brand name, include a clear offer or CTA, and always end with opt-out instructions: "Jibu STOP kusimamisha."
Send bulk via a single API call
Pass an array of up to 10,000 numbers in to. MojaWave handles batching, routing, and rate limits automatically.
Process inbound STOP replies
MojaWave forwards inbound SMS to your webhook. Detect "STOP" and immediately remove from your list — required within 24 hours by TCRA regulations.
// 1. Opted-in list only const phones = (await db.customers.findMany({ where: { smsOptIn: true, smsOptOut: false }, select: { phone: true } })).map(r => r.phone); // 2. Send bulk campaign const campaign = await mojawave.sms.send({ to: phones, // up to 10,000 from: 'Duka Masta', message: 'Duka Masta: 20% off leo! ' + 'duka.co/promo · Jibu STOP.', send_at: '2025-01-15T09:00:00+03:00' }); // 3. Handle inbound STOP replies app.post('/webhooks/inbound', async (req, res) => { const { from, message } = req.body; if (message.trim().toUpperCase() === 'STOP') await db.customers.update( { phone: from }, { smsOptOut: true } ); res.sendStatus(200); });
📱 SMS Preview
Duka Masta
09:00 · Delivered ✓✓
{ "event": "inbound.sms",
"from": "+255755123456",
"to": "Duka Masta",
"message": "STOP",
"network": "vodacom",
"ts": "2025-01-15T09:03:21Z" }Opt-out is not optional
TCRA regulations require every marketing SMS to include a clear opt-out instruction. Failure risks your Sender ID being blacklisted.
Schedule for peak hours
9–11am and 4–6pm generate the highest response rates in Tanzania. Use send_at to schedule in advance.
Segment, don't blast all
Send promos only to customers who've purchased in the last 90 days. Smaller, relevant audiences convert far better.
Track campaign metrics
Use unique UTM links per campaign. Combine delivery receipts with your analytics to measure true ROI per send.
And so much more...
Flexible APIs for every customer touchpoint.
Invoices & Receipts
Send payment confirmations and invoices with attached PDFs automatically after each transaction.
Welcome Emails
Onboard new users with a branded welcome series triggered by account creation events.
Abandoned Carts
Recover lost revenue by automatically messaging users who left items in their digital basket.
Appointment Reminders
Reduce no-shows with automated reminders sent 24 hours before a scheduled meeting or booking.
Ready to ship?
Send your first SMS today
Register and get 3 free SMS credits — no credit card required.