Three-tier provider stack. Primary, regional failover, global standby. Health scores on a 15-minute rolling window. Automatic failover. You never touch a routing config.
SMS and WhatsApp from the same credentials. The channel field routes the message. Integration logic stays identical.
# Send an SMS via Waht Messaging curl -X POST https://api.waht.co.ke/v1/messages \ -H "Authorization: Bearer wk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "to": "+254712345678", "channel": "sms", "body": "Your order #4821 has shipped.", "sender_id": "WAHT" }' # Response · returned in < 200ms { "message_id": "msg_01HXKR3...", "status": "queued", "provider": "tier_1", "cost_kes": "[confirmed at onboarding]" }
# Send via WhatsApp · same endpoint, channel changes curl -X POST https://api.waht.co.ke/v1/messages \ -H "Authorization: Bearer wk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "to": "+254712345678", "channel": "whatsapp", "body": "Your order #4821 has shipped.", "template_id": "order_shipped_v1" }' # Response { "message_id": "msg_01HXKR4...", "status": "queued", "provider": "meta", "cost_kes": 0.00 }
# Step 1 · send OTP (value never returned) curl -X POST https://api.waht.co.ke/v1/otp/send \ -H "Authorization: Bearer wk_live_your_key_here" \ -d '{ "phone": "+254712345678", "purpose": "login", "expiry_seconds": 300 }' { "otp_id": "otp_01HXKR5..." } # Step 2 · verify (returns success flag only) curl -X POST https://api.waht.co.ke/v1/otp/verify \ -d '{ "otp_id": "otp_01HXKR5...", "code": "847291" }' { "success": true }
# Initiate M-Pesa STK Push top-up curl -X POST https://api.waht.co.ke/v1/billing/topup \ -H "Authorization: Bearer wk_billing_your_key_here" \ -d '{ "amount_kes": 5000, "phone": "+254712345678" }' { "transaction_id": "txn_01HXKR6...", "status": "pending" } # Daraja callback → wallet credited automatically # No polling required. Balance updates via webhook.
Fraud scoring runs synchronously on every inbound request. Before any enqueue. Before any provider dispatch. A composited risk score determines the outcome.
Redis blocks carry no automatic expiry. Ops lifts them via the admin dashboard with a mandatory resolution note. block_lifted_by and block_lifted_at written atomically.
OTP is the highest-sensitivity surface in messaging infrastructure. These are architectural constraints, not configuration options.
// Generation · value computed in memory only const otp = crypto.randomInt(100000, 999999); const salt = crypto.randomBytes(16).toString('hex'); const hash = sha256(otp + salt); // Stored: hash + salt · NOT the value INSERT INTO otps (otp_hash, salt, expires_at, ...) VALUES (hash, salt, NOW() + INTERVAL '5 min', ...); // Value delivered to recipient via SMS only sendSMS(phone, `Your code: ${otp}`); // Verification · DB enforces expiry, not app layer SELECT * FROM otps WHERE id = otp_id AND status = 'active' AND expires_at > NOW() -- not checked in JS AND used_at IS NULL; // Response · always this shape, nothing more { "success": true } { "success": false } // same for expired, wrong, used
Top up with M-Pesa. Spend by the message. Per-SMS cost is set by whichever tier the routing engine selects. Automatically, in real time.
Waht Messaging is in controlled access during MVP. Keys are provisioned by the ops team. We confirm your routing config, register your sender IDs, and walk your team through first dispatch.