Kenya has one of Africa's most digitally connected consumer bases. WhatsApp penetration in Kenya sits above 90% among smartphone users, making it the single most important business communication channel in the country — more widely used than email, SMS, or any other platform.
But the market has matured. In 2023 and 2024, early-adopter businesses deployed simple rule-based bots that handled FAQs and order confirmations. By 2025, these bots became table stakes. In 2026, the competitive edge belongs to businesses that deploy AI agents — systems that understand context, make decisions, trigger payments, and automate multi-step business workflows without human intervention.
The Business Case: Numbers That Matter
- Businesses using WhatsApp automation report 35–60% reduction in inbound support volume, according to Meta Business Reports.
- Response times drop from an average of 4 hours to under 2 seconds for common queries.
- M-Pesa processed over KES 36 trillion in 2024 (Safaricom Annual Report), making payment-integrated bots a strategic necessity — not a nice-to-have.
- Businesses combining AI reasoning with M-Pesa automation are reporting 22–40% improvements in payment conversion rates compared to manual invoicing.
What Separates a 2026 AI Bot from a 2022 Chatbot
| Feature | 2022 Chatbot | 2026 AI Agent |
|---|---|---|
| Logic | Rule-based decision trees | LLM reasoning + tool calling |
| Memory | Session only | Persistent conversation history |
| Payments | STK push only | C2B, B2C, QR, reconciliation |
| Escalation | Manual keyword triggers | AI-detected intent + seamless handoff |
| Languages | English only | English, Swahili, Sheng support |
| Compliance | Basic logging | Full audit trail, encrypted tokens |
| Failure handling | Crashes silently | Retry logic, alerting, fallbacks |
This guide will walk you through building the 2026 version — a production-grade, payment-ready AI agent on WhatsApp.
The 2026 Architecture: From Chatbot to AI Agent
Before writing a single line of code, understand the conceptual shift happening in conversational AI architecture.
Traditional Chatbot Pattern (Avoid This)
User message → Keyword match → Hardcoded response
This breaks the moment a user says "nipe balance yangu" instead of "check balance." The bot fails. The user is frustrated.
Modern AI Agent Pattern (Build This)
User message
→ Intent classification (LLM)
→ Tool selection (agent orchestrator)
→ Tool execution (payment, DB, CRM)
→ Structured response formatting
→ WhatsApp message delivery
→ Context stored for next turn
The AI agent doesn't just reply — it reasons, acts, and remembers.
The Agentic Loop Explained
The core concept borrowed from frameworks like LangChain and AutoGen is the Reason → Act → Observe loop:
- Reason: The LLM reads the conversation history and the user's latest message, then decides what to do next.
- Act: It selects and calls a tool — a PHP function, an M-Pesa API call, a database query.
- Observe: It reads the tool's output and decides whether to call another tool or respond to the user.
- Repeat until the task is complete or a human handoff is needed.
This pattern can handle complex requests like: "Nirefundie pesa yangu ya order 1042 — niliambia hamna delivery leo." — which requires verifying the order, checking refund eligibility, triggering M-Pesa B2C, and confirming the payout in a single conversation turn.
Full System Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ WHATSAPP USER (Kenya) │
└───────────────────────────┬─────────────────────────────────────┘
│ HTTPS Webhook
▼
┌─────────────────────────────────────────────────────────────────┐
│ META CLOUD API / TWILIO WHATSAPP │
│ (Message routing + delivery) │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LARAVEL APPLICATION │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Webhook │ │ Message │ │ Conversation │ │
│ │ Controller │→ │ Processor │→ │ Context Manager │ │
│ └─────────────┘ └──────────────┘ └────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ AI AGENT ORCHESTRATOR │ │
│ │ Intent Classification → Tool Selection → Response Format │ │
│ └────────────────┬───────────────────────────────────────────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ M-Pesa │ │Database │ │ CRM / │ │
│ │ Tools │ │ Tools │ │ ERP │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
└───────┼───────────┼───────────┼──────────────────────────────── ┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌──────────┐ ┌──────────────────────┐
│ SAFARICOM│ │ MySQL / │ │ OpenAI API │
│ DARAJA │ │ Redis │ │ (GPT-4o / Claude) │
│ API │ │ │ │ │
└───────────┘ └──────────┘ └──────────────────────┘
Prerequisites and Stack Overview
Before you begin, ensure you have the following in place.
Required Accounts and Access
- Meta Business Account with verified WhatsApp Business API access, or a Twilio WhatsApp sandbox for development
- Safaricom Daraja Account — register at developer.safaricom.co.ke with a verified business
- OpenAI API Key — or equivalent (Anthropic Claude, Google Gemini)
- A publicly accessible server — the WhatsApp webhook requires a valid HTTPS URL
Technology Stack
| Layer | Technology | Why |
|---|---|---|
| Backend Framework | Laravel 11+ | Robust queues, jobs, Eloquent ORM |
| AI Provider | OpenAI GPT-4o | Best tool-calling support in 2026 |
| WhatsApp Provider | Meta Cloud API | Official, lowest cost in Kenya |
| Payment Layer | Safaricom Daraja | M-Pesa C2B, B2C, STK, QR |
| Database | MySQL + Redis | Persistence + caching/sessions |
| Queue System | Laravel Horizon + Redis | Async message processing |
| Hosting | AWS / DigitalOcean / Hetzner | HTTPS, uptime, Africa-region latency |
PHP Packages Required
composer require guzzlehttp/guzzle
composer require openai-php/client
composer require laravel/horizon
Step 1: Setting Up WhatsApp Business API in Kenya
Option A: Meta Cloud API (Recommended — Free Hosting)
Meta Cloud API hosts the WhatsApp gateway on Meta's infrastructure. You pay only for conversations, not for servers.
Step-by-step:
- Go to developers.facebook.com and create an app.
- Add the WhatsApp product to your app.
- Under WhatsApp > Getting Started, note your:
WHATSAPP_PHONE_NUMBER_IDWHATSAPP_ACCESS_TOKENWHATSAPP_VERIFY_TOKEN(you set this yourself)
- Set your Webhook URL to
https://yourdomain.co.ke/api/whatsapp/webhook - Subscribe to the
messageswebhook field.
Kenya-specific note: Meta requires a verified business for sending messages outside the 24-hour window. Ensure your business is registered with the Kenya Revenue Authority (KRA) for verification purposes.
Option B: Twilio (Recommended for Development/Staging)
Twilio's WhatsApp sandbox lets you test without Meta Business verification. Use this during development, then migrate to Meta Cloud API for production.
Webhook Verification in Laravel
Create the webhook controller:
// routes/api.php
Route::get('/whatsapp/webhook', [WhatsAppController::class, 'verify']);
Route::post('/whatsapp/webhook', [WhatsAppController::class, 'receive']);
// app/Http/Controllers/WhatsAppController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WhatsAppController extends Controller
{
/**
* Meta webhook verification handshake
*/
public function verify(Request $request)
{
$mode = $request->query('hub_mode');
$token = $request->query('hub_verify_token');
$challenge = $request->query('hub_challenge');
if ($mode === 'subscribe' && $token === config('whatsapp.verify_token')) {
return response($challenge, 200);
}
return response('Forbidden', 403);
}
/**
* Receive and dispatch incoming messages
*/
public function receive(Request $request)
{
$payload = $request->all();
// Validate signature
$this->validateSignature($request);
// Dispatch to queue for async processing
ProcessWhatsAppMessage::dispatch($payload);
return response('OK', 200); // Always return 200 fast
}
private function validateSignature(Request $request): void
{
$signature = $request->header('X-Hub-Signature-256');
$payload = $request->getContent();
$expected = 'sha256=' . hash_hmac('sha256', $payload, config('whatsapp.app_secret'));
if (!hash_equals($expected, $signature ?? '')) {
abort(403, 'Invalid signature');
}
}
}
Critical: Always return HTTP 200 immediately from your webhook endpoint. WhatsApp will retry failed deliveries aggressively, causing duplicate message processing. Dispatch to a queue and process asynchronously.
Step 2: Setting Up Your Laravel Backend
Environment Configuration
Add to your .env file:
# WhatsApp
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id
WHATSAPP_ACCESS_TOKEN=your_permanent_access_token
WHATSAPP_VERIFY_TOKEN=your_custom_verify_token
WHATSAPP_APP_SECRET=your_app_secret
# OpenAI
OPENAI_API_KEY=sk-your-openai-key
# M-Pesa Daraja
MPESA_CONSUMER_KEY=your_consumer_key
MPESA_CONSUMER_SECRET=your_consumer_secret
MPESA_SHORTCODE=your_shortcode
MPESA_PASSKEY=your_passkey
MPESA_ENV=sandbox # change to production when live
MPESA_B2C_INITIATOR_NAME=your_initiator
MPESA_B2C_SECURITY_CREDENTIAL=your_encrypted_credential
MPESA_CALLBACK_BASE_URL=https://yourdomain.co.ke
Database Migrations
// Database: conversations table
Schema::create('conversations', function (Blueprint $table) {
$table->id();
$table->string('phone_number')->index();
$table->string('user_name')->nullable();
$table->json('history')->default('[]'); // Conversation history for AI context
$table->string('current_intent')->nullable();
$table->json('metadata')->default('{}'); // Order IDs, pending actions, etc.
$table->timestamp('last_active_at')->nullable();
$table->timestamps();
});
// Database: transactions table
Schema::create('whatsapp_transactions', function (Blueprint $table) {
$table->id();
$table->string('phone_number')->index();
$table->string('transaction_type'); // stk_push, c2b, b2c, qr
$table->string('transaction_id')->unique()->nullable();
$table->decimal('amount', 10, 2);
$table->string('reference')->nullable();
$table->string('status')->default('pending'); // pending, completed, failed
$table->json('raw_callback')->nullable();
$table->timestamps();
});
The Message Processor Job
// app/Jobs/ProcessWhatsAppMessage.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Services\WhatsApp\MessageParser;
use App\Services\AI\AgentOrchestrator;
class ProcessWhatsAppMessage implements ShouldQueue
{
use Queueable;
public $tries = 3;
public $backoff = [5, 30, 60]; // Retry with exponential backoff
public function __construct(private array $payload) {}
public function handle(MessageParser $parser, AgentOrchestrator $agent): void
{
$message = $parser->parse($this->payload);
if (!$message || $message->type !== 'text') {
return; // Handle only text for now
}
// Run the AI agent and send reply
$reply = $agent->run($message->from, $message->text, $message->name);
app(WhatsAppService::class)->sendMessage($message->from, $reply);
}
}
Step 3: Integrating OpenAI for AI Reasoning
The core of your AI agent is the prompt design and tool definitions. This is where most implementations fail — they use vague system prompts and get inconsistent results.
Designing Your System Prompt
Your system prompt must be explicit about the business context, available tools and when to use them, output format expectations, language handling (Swahili/Sheng tolerance), and guardrails.
// app/Services/AI/SystemPrompt.php
namespace App\Services\AI;
class SystemPrompt
{
public static function build(string $businessName, string $businessType): string
{
return <<<PROMPT
You are an AI assistant for {$businessName}, a {$businessType} business operating in Kenya.
## YOUR ROLE
You handle customer inquiries, process payments, check order statuses, and manage refunds via WhatsApp.
## LANGUAGE
- Respond in the same language the customer uses (English, Swahili, or informal Sheng).
- Be warm, professional, and concise. Avoid long paragraphs.
- Use simple language. Avoid jargon.
## AVAILABLE TOOLS
You have access to the following tools. Use them when appropriate:
1. `check_order_status` — Look up an order by order ID or phone number.
2. `initiate_stk_push` — Send an M-Pesa payment prompt to the customer.
3. `process_refund` — Initiate an M-Pesa B2C refund after verification.
4. `check_balance` — Retrieve a customer's account or wallet balance.
5. `escalate_to_human` — Transfer the conversation to a human agent.
## DECISION RULES
- ALWAYS verify the customer's identity before triggering any payment action.
- NEVER share another customer's data.
- If you cannot confidently handle a request, use `escalate_to_human`.
- For refund requests, always call `check_order_status` first.
## OUTPUT FORMAT
Respond with a JSON object in this exact structure:
{
"action": "reply" | "tool_call",
"message": "Your message to the customer",
"tool": "tool_name_if_action_is_tool_call",
"tool_params": { "param": "value" }
}
PROMPT;
}
}
The OpenAI Client Service
// app/Services/AI/OpenAIClient.php
namespace App\Services\AI;
use OpenAI\Client;
class OpenAIClient
{
protected Client $client;
public function __construct()
{
$this->client = \OpenAI::client(config('openai.api_key'));
}
public function chat(array $messages, array $tools = []): array
{
$params = [
'model' => 'gpt-4o',
'messages' => $messages,
'temperature' => 0.3, // Lower = more deterministic for business logic
'max_tokens' => 800,
];
if (!empty($tools)) {
$params['tools'] = $tools;
$params['tool_choice'] = 'auto';
}
$response = $this->client->chat()->create($params);
return $response->choices[0]->message->toArray();
}
}
Step 4: Building Agentic Workflows
This is the architectural centrepiece of your AI WhatsApp bot. The agent orchestrator manages the Reason → Act → Observe loop.
The Agent Orchestrator
// app/Services/AI/AgentOrchestrator.php
namespace App\Services\AI;
use App\Models\Conversation;
use App\Services\Tools\ToolRegistry;
class AgentOrchestrator
{
private int $maxIterations = 5; // Prevent infinite loops
public function __construct(
private OpenAIClient $ai,
private ToolRegistry $tools,
private SystemPrompt $prompt
) {}
public function run(string $phone, string $userMessage, string $userName): string
{
// 1. Load or create conversation context
$conversation = Conversation::firstOrCreate(
['phone_number' => $phone],
['user_name' => $userName, 'history' => []]
);
$history = $conversation->history;
// 2. Append user message to history
$history[] = ['role' => 'user', 'content' => $userMessage];
// 3. Build message array for OpenAI
$messages = array_merge(
[['role' => 'system', 'content' => SystemPrompt::build('Your Business', 'ecommerce')]],
$history
);
// 4. Agentic loop
$iterations = 0;
$finalReply = "Samahani, kuna tatizo. Jaribu tena."; // Fallback
while ($iterations < $this->maxIterations) {
$iterations++;
$aiResponse = $this->ai->chat($messages, $this->tools->definitions());
// If AI wants to call a tool
if (isset($aiResponse['tool_calls'])) {
$toolCall = $aiResponse['tool_calls'][0];
$toolName = $toolCall['function']['name'];
$toolParams = json_decode($toolCall['function']['arguments'], true);
// Execute the tool
$toolResult = $this->tools->execute($toolName, $toolParams, $phone);
// Append tool call + result to messages for next iteration
$messages[] = $aiResponse; // Assistant's tool call
$messages[] = [
'role' => 'tool',
'tool_call_id' => $toolCall['id'],
'content' => json_encode($toolResult),
];
continue; // Let the AI reason about the tool result
}
// AI has a final reply — break the loop
$finalReply = $aiResponse['content'];
break;
}
// 5. Persist updated history (limit to last 20 turns to control token cost)
$history[] = ['role' => 'assistant', 'content' => $finalReply];
$conversation->history = array_slice($history, -20);
$conversation->last_active_at = now();
$conversation->save();
return $finalReply;
}
}
The Tool Registry
// app/Services/Tools/ToolRegistry.php
namespace App\Services\Tools;
use App\Services\MPesa\MPesaService;
use App\Services\Order\OrderService;
class ToolRegistry
{
public function __construct(
private MPesaService $mpesa,
private OrderService $orders
) {}
/**
* Tool definitions sent to OpenAI for function calling
*/
public function definitions(): array
{
return [
[
'type' => 'function',
'function' => [
'name' => 'check_order_status',
'description' => 'Look up the status and details of a customer order by order ID.',
'parameters' => [
'type' => 'object',
'properties' => [
'order_id' => ['type' => 'string', 'description' => 'The order ID to look up'],
],
'required' => ['order_id'],
],
],
],
[
'type' => 'function',
'function' => [
'name' => 'process_refund',
'description' => 'Initiate an M-Pesa B2C refund to the customer after verifying eligibility.',
'parameters' => [
'type' => 'object',
'properties' => [
'order_id' => ['type' => 'string'],
'amount' => ['type' => 'number', 'description' => 'Amount to refund in KES'],
'reason' => ['type' => 'string'],
],
'required' => ['order_id', 'amount', 'reason'],
],
],
],
[
'type' => 'function',
'function' => [
'name' => 'initiate_stk_push',
'description' => 'Send an M-Pesa STK push payment request to the customer.',
'parameters' => [
'type' => 'object',
'properties' => [
'amount' => ['type' => 'number'],
'description' => ['type' => 'string', 'description' => 'Payment description for the customer'],
],
'required' => ['amount', 'description'],
],
],
],
[
'type' => 'function',
'function' => [
'name' => 'escalate_to_human',
'description' => 'Transfer the conversation to a human support agent when the AI cannot resolve the issue.',
'parameters' => [
'type' => 'object',
'properties' => [
'reason' => ['type' => 'string', 'description' => 'Why escalation is needed'],
],
'required' => ['reason'],
],
],
],
];
}
/**
* Execute a named tool and return its result
*/
public function execute(string $name, array $params, string $phone): array
{
return match ($name) {
'check_order_status' => $this->orders->getStatus($params['order_id']),
'process_refund' => $this->mpesa->b2cPayout($phone, $params['amount'], $params['reason']),
'initiate_stk_push' => $this->mpesa->stkPush($phone, $params['amount'], $params['description']),
'escalate_to_human' => $this->escalateToHuman($phone, $params['reason']),
default => ['error' => 'Unknown tool'],
};
}
private function escalateToHuman(string $phone, string $reason): array
{
Conversation::where('phone_number', $phone)
->update(['current_intent' => 'human_escalated']);
return ['status' => 'escalated', 'message' => 'Human agent will join shortly.'];
}
}
Step 5: Advanced M-Pesa Integration (C2B, B2C, QR Codes)
Most tutorials stop at STK push. Production Kenyan platforms require all four payment flows. Here's how to implement each.
The M-Pesa Service Base
// app/Services/MPesa/MPesaService.php
namespace App\Services\MPesa;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
class MPesaService
{
private string $baseUrl;
public function __construct()
{
$this->baseUrl = config('mpesa.env') === 'production'
? 'https://api.safaricom.co.ke'
: 'https://sandbox.safaricom.co.ke';
}
/**
* Get OAuth access token (cached for 55 minutes)
*/
public function getAccessToken(): string
{
return Cache::remember('mpesa_token', 3300, function () {
$response = Http::withBasicAuth(
config('mpesa.consumer_key'),
config('mpesa.consumer_secret')
)->get("{$this->baseUrl}/oauth/v1/generate?grant_type=client_credentials");
return $response->json('access_token');
});
}
// ─────────────────────────────────────────────────
// 1. STK PUSH (Lipa Na M-Pesa Online)
// ─────────────────────────────────────────────────
public function stkPush(string $phone, float $amount, string $description): array
{
$timestamp = now()->format('YmdHis');
$password = base64_encode(
config('mpesa.shortcode') . config('mpesa.passkey') . $timestamp
);
$phone = $this->normalizePhone($phone);
$response = Http::withToken($this->getAccessToken())
->post("{$this->baseUrl}/mpesa/stkpush/v1/processrequest", [
'BusinessShortCode' => config('mpesa.shortcode'),
'Password' => $password,
'Timestamp' => $timestamp,
'TransactionType' => 'CustomerPayBillOnline',
'Amount' => (int) $amount,
'PartyA' => $phone,
'PartyB' => config('mpesa.shortcode'),
'PhoneNumber' => $phone,
'CallBackURL' => config('mpesa.callback_base_url') . '/api/mpesa/stk-callback',
'AccountReference' => 'Order-' . uniqid(),
'TransactionDesc' => $description,
]);
return $response->json();
}
// ─────────────────────────────────────────────────
// 2. C2B (Customer to Business — Paybill/Till)
// ─────────────────────────────────────────────────
public function registerC2BUrls(): array
{
return Http::withToken($this->getAccessToken())
->post("{$this->baseUrl}/mpesa/c2b/v1/registerurl", [
'ShortCode' => config('mpesa.shortcode'),
'ResponseType' => 'Completed',
'ConfirmationURL' => config('mpesa.callback_base_url') . '/api/mpesa/c2b-confirmation',
'ValidationURL' => config('mpesa.callback_base_url') . '/api/mpesa/c2b-validation',
])->json();
}
public function handleC2BConfirmation(array $payload): void
{
$transaction = WhatsAppTransaction::where('reference', $payload['BillRefNumber'])
->where('status', 'pending')
->first();
if ($transaction) {
$transaction->update([
'status' => 'completed',
'transaction_id' => $payload['TransID'],
'raw_callback' => $payload,
]);
app(WhatsAppService::class)->sendMessage(
$transaction->phone_number,
"✅ Malipo ya KES {$payload['TransAmount']} yamepokelewa. Asante!"
);
}
}
// ─────────────────────────────────────────────────
// 3. B2C (Business to Customer — Refunds/Payouts)
// ─────────────────────────────────────────────────
public function b2cPayout(string $phone, float $amount, string $remarks): array
{
$phone = $this->normalizePhone($phone);
$response = Http::withToken($this->getAccessToken())
->post("{$this->baseUrl}/mpesa/b2c/v3/paymentrequest", [
'OriginatorConversationID' => uniqid('b2c_'),
'InitiatorName' => config('mpesa.b2c_initiator_name'),
'SecurityCredential' => config('mpesa.b2c_security_credential'),
'CommandID' => 'BusinessPayment',
'Amount' => (int) $amount,
'PartyA' => config('mpesa.shortcode'),
'PartyB' => $phone,
'Remarks' => $remarks,
'QueueTimeOutURL' => config('mpesa.callback_base_url') . '/api/mpesa/b2c-timeout',
'ResultURL' => config('mpesa.callback_base_url') . '/api/mpesa/b2c-result',
'Occasion' => 'Refund',
]);
return $response->json();
}
// ─────────────────────────────────────────────────
// 4. DYNAMIC QR CODE GENERATION
// ─────────────────────────────────────────────────
public function generateDynamicQR(string $orderId, float $amount): array
{
$response = Http::withToken($this->getAccessToken())
->post("{$this->baseUrl}/mpesa/qrcode/v1/generate", [
'MerchantName' => config('mpesa.merchant_name'),
'RefNo' => $orderId,
'Amount' => (int) $amount,
'TrxCode' => 'BG', // Buy goods
'CPI' => config('mpesa.shortcode'),
'Size' => '300',
]);
return $response->json();
}
// ─────────────────────────────────────────────────
// UTILITY: Phone number normalization for Kenya
// ─────────────────────────────────────────────────
private function normalizePhone(string $phone): string
{
$phone = preg_replace('/\D/', '', $phone);
if (str_starts_with($phone, '0')) {
$phone = '254' . substr($phone, 1);
} elseif (str_starts_with($phone, '+')) {
$phone = substr($phone, 1);
}
return $phone;
}
}
Step 6: Payment Reconciliation Logic
This is where most production systems break down. Without proper reconciliation, you'll have duplicate payouts, payments received but orders not fulfilled, failed STK pushes that aren't retried, and B2C payouts that time out silently.
Reconciliation Service
// app/Services/MPesa/ReconciliationService.php
namespace App\Services\MPesa;
class ReconciliationService
{
/**
* Called when STK push callback arrives
*/
public function handleSTKCallback(array $body): void
{
$result = $body['Body']['stkCallback'];
$checkoutId = $result['CheckoutRequestID'];
$transaction = WhatsAppTransaction::where('transaction_id', $checkoutId)->first();
if (!$transaction) {
\Log::warning('STK callback for unknown transaction', ['id' => $checkoutId]);
return;
}
if ($result['ResultCode'] === 0) {
$items = collect($result['CallbackMetadata']['Item'])
->pluck('Value', 'Name');
$transaction->update([
'status' => 'completed',
'transaction_id' => $items['MpesaReceiptNumber'],
'raw_callback' => $result,
]);
app(OrderService::class)->fulfill($transaction->reference);
app(WhatsAppService::class)->sendMessage(
$transaction->phone_number,
"✅ Malipo imekamilika! Receipt: {$items['MpesaReceiptNumber']}. Order yako inashughulikiwa."
);
} else {
$transaction->update(['status' => 'failed', 'raw_callback' => $result]);
app(WhatsAppService::class)->sendMessage(
$transaction->phone_number,
"❌ Malipo hayakufanikiwa. Jaribu tena au wasiliana nasi."
);
}
}
/**
* Scheduled job: Check for stuck pending transactions
*/
public function reconcileStuckTransactions(): void
{
$stuck = WhatsAppTransaction::where('status', 'pending')
->where('created_at', '<', now()->subMinutes(15))
->get();
foreach ($stuck as $tx) {
$status = app(MPesaService::class)->querySTKStatus($tx->checkout_request_id);
if ($status['ResultCode'] === 0) {
$tx->update(['status' => 'completed']);
app(OrderService::class)->fulfill($tx->reference);
} else {
$tx->update(['status' => 'failed']);
}
}
}
}
Schedule the reconciliation job in app/Console/Kernel.php:
$schedule->call(fn() => app(ReconciliationService::class)->reconcileStuckTransactions())
->everyFiveMinutes();
Step 7: Security and Compliance
When your WhatsApp bot handles real M-Pesa transactions, security is non-negotiable.
Webhook Signature Validation
Always validate the X-Hub-Signature-256 header on every WhatsApp webhook (covered in Step 1). Never process unsigned requests.
Daraja API Token Security
Never store the Daraja access token in your database. Use Cache::remember() in memory only. Rotate your SecurityCredential (B2C) whenever staff with access change. The B2C SecurityCredential must be encrypted with Safaricom's public certificate:
public function encryptSecurityCredential(string $plaintext): string
{
$certPath = storage_path('app/safaricom_cert.cer');
$cert = openssl_x509_read(file_get_contents($certPath));
openssl_x509_export($cert, $pubKey);
openssl_public_encrypt($plaintext, $encrypted, $pubKey, OPENSSL_PKCS1_PADDING);
return base64_encode($encrypted);
}
Rate Limiting
RateLimiter::for('whatsapp-webhook', function (Request $request) {
return Limit::perMinute(100)->by($request->ip());
});
Audit Logging
Log every financial action with enough context to reconstruct what happened:
AuditLog::create([
'action' => 'b2c_payout_initiated',
'phone' => $phone,
'amount' => $amount,
'order_id' => $orderId,
'initiated_by' => 'ai_agent',
'ip' => request()->ip(),
'created_at' => now(),
]);
Compliance Checklist for Kenya
- Data processed in accordance with Kenya Data Protection Act (2019)
- Customer consent captured before storing conversation data
- Financial records retained for 7 years (per KRA requirements)
- B2C payouts above KES 75,000 may trigger AML reporting obligations
- WhatsApp Terms of Service Commerce Policy reviewed for your product category
Step 8: Conversation Memory and Context Management
The AI agent needs memory to have coherent multi-turn conversations. Without it, every message is treated as a new conversation — "Yes" as a reply to "Do you want a refund?" makes no sense without context.
How We Store and Trim History
The Conversation model stores history as JSON. To avoid runaway token costs, store only the last 20 message turns, store key facts (verified name, order IDs, pending actions) separately in metadata, and summarize older turns in a background job for very long sessions.
// Summarize old conversation history to save tokens
public function summarizeHistory(Conversation $conversation): void
{
if (count($conversation->history) < 30) {
return;
}
$oldHistory = array_slice($conversation->history, 0, 20);
$summary = app(OpenAIClient::class)->chat([
['role' => 'system', 'content' => 'Summarize this conversation in 3 bullet points. Include any unresolved issues or order references.'],
['role' => 'user', 'content' => json_encode($oldHistory)],
]);
$recentHistory = array_slice($conversation->history, -10);
$summaryMessage = ['role' => 'system', 'content' => 'Previous conversation summary: ' . $summary['content']];
$conversation->history = array_merge([$summaryMessage], $recentHistory);
$conversation->save();
}
Step 9: Human Escalation Fallback
An AI agent that cannot escalate is dangerous in production. Always build a clean handoff.
When to Escalate
The AI should escalate when the user explicitly asks for a human, the issue involves a complaint about a previous interaction, the AI has failed to resolve the issue after 3 turns, the request involves an amount above a configurable threshold, or legal/regulatory issues are mentioned.
Implementing Escalation
// app/Services/Escalation/EscalationService.php
class EscalationService
{
public function escalate(string $phone, string $reason, array $history): void
{
// 1. Flag the conversation
Conversation::where('phone_number', $phone)
->update(['current_intent' => 'human_escalated']);
// 2. Notify your support team
Notification::route('slack', config('slack.support_channel'))
->notify(new HumanEscalationRequired($phone, $reason, $history));
// 3. Tell the customer
app(WhatsAppService::class)->sendMessage(
$phone,
"Nimesimamisha mazungumzo haya kwa ajili ya wakala wa binadamu. " .
"Mtu wetu atakuwasiliana ndani ya dakika 5-10. 🙏"
);
// 4. Optional: Create a support ticket in your CRM
}
}
Step 10: Deployment, Monitoring, and Scaling
Hosting Requirements for Kenya-Facing Bots
For production you need HTTPS (mandatory for WhatsApp webhooks and Daraja callbacks), an uptime SLA of 99.9%+ (payment callbacks cannot be missed), a server in or near Kenya (Hetzner Finland, AWS af-south-1 Cape Town, or DigitalOcean Frankfurt all offer low latency to Kenya), and queue workers running continuously via Laravel Horizon.
Horizon Configuration
// config/horizon.php
'environments' => [
'production' => [
'whatsapp-worker' => [
'connection' => 'redis',
'queue' => ['whatsapp', 'mpesa-callbacks'],
'balance' => 'auto',
'processes' => 10,
'tries' => 3,
],
],
],
Monitoring
Set up alerts for queue depth exceeding 100 messages, M-Pesa callback failures above 5 in 10 minutes, OpenAI API error rate above 1%, and a growing failed_jobs table.
Recommended tools: Laravel Telescope (development), Sentry (production errors), UptimeRobot (endpoint monitoring), Grafana + Prometheus (metrics).
Real-World Use Cases in Kenya
Fintech and SACCO Platforms
Members need 24/7 access to balance queries, loan applications, and repayment reminders. A production setup uses C2B for loan repayments matched to member accounts, B2C for loan disbursements, AI for eligibility pre-screening, and human escalation for appeals. SACCOs piloting this model report 70%+ reduction in call centre volume.
E-Commerce Platforms
Customers want order status, payment, and refund handling without calling. The solution combines STK push for checkout, dynamic QR for retail/agent payments, AI order tracking integration, and B2C for refunds processed within 24 hours.
Transport and Matatu Industry
Dynamic QR codes are generated per booking. C2B confirmation triggers ticket issuance, the bot sends a PDF e-ticket via WhatsApp, and the AI handles seat change requests and cancellations.
Healthcare and Clinic Management
STK push handles consultation fees, an AI-powered booking system integrates with calendar tools, WhatsApp sends medication schedule reminders, and human escalation connects patients to clinical staff for medical questions.
Cost Breakdown: AI Bot vs. Human Agents
For a mid-sized Kenyan business handling 10,000 WhatsApp conversations per month, the economics look like this.
Human Agent Option
| Item | Monthly Cost (KES) |
|---|---|
| 3 support agents (salaries) | 180,000 |
| Office overhead | 30,000 |
| Training and turnover | 15,000 |
| Total | 225,000 |
AI WhatsApp Bot Option
| Item | Monthly Cost (KES) |
|---|---|
| WhatsApp conversation fees (~10,000 @ $0.011) | ~1,430 |
| OpenAI API (avg 800 tokens/conversation) | ~7,800 |
| Server hosting (VPS + Redis) | 8,000 |
| Developer maintenance (5 hrs/month) | 25,000 |
| Total | ~42,230 |
Savings: ~KES 182,770/month (~81% cost reduction). Even with 1–2 human agents retained for complex escalations, the economics strongly favour automation.
Common Mistakes and How to Avoid Them
1. Synchronous Webhook Processing
Processing the WhatsApp message inside the webhook controller means that if your logic takes more than 5 seconds, Meta will retry the webhook and cause duplicate messages. Always dispatch to a queue and return 200 OK immediately.
2. Not Validating Webhook Signatures
Accepting any POST request to your webhook URL allows attackers to spoof payment confirmations or flood your system. Always validate X-Hub-Signature-256.
3. Storing M-Pesa Access Tokens in the Database
Tokens expire every 60 minutes. Stale database tokens cause silent API failures, and stored tokens are a security risk. Use Cache::remember() with a 55-minute TTL.
4. No Conversation Context
Treating every WhatsApp message independently means the AI cannot handle multi-step workflows. Store and pass conversation history as shown in Step 8.
5. No Fallback or Human Escalation
Building a bot that tries to handle everything means the AI will hallucinate or give wrong information on edge cases, damaging customer trust. Build explicit escalation triggers as shown in Step 9.
6. Ignoring Swahili and Sheng
A large portion of Kenyan WhatsApp users communicate in Swahili or informal Sheng. Prompting the AI only in English makes the bot feel foreign and unhelpful. Include language detection in your system prompt and test specifically in Swahili.
2026 Production Checklist
WhatsApp Setup
- WhatsApp Business account verified by Meta
- Webhook signature validation implemented
- 200 OK returned immediately from webhook
- Messages dispatched to async queue
- Message deduplication logic in place (check message IDs)
AI Agent
- System prompt tested in English and Swahili
- Tool definitions validated with real tool calls
- Conversation history stored and trimmed correctly
- Fallback response defined for AI errors
- Human escalation trigger implemented
M-Pesa Integration
- STK push tested in sandbox and production
- C2B confirmation URL registered with Safaricom
- B2C security credential properly encrypted
- Callback endpoints return
{"ResultCode": 0}within 5 seconds - Transaction reconciliation job scheduled
- Stuck transaction handling tested
Security
- All API keys in
.env, not in code - B2C credential encrypted with Safaricom certificate
- Rate limiting on all public endpoints
- Audit logging for all financial actions
- No PII logged in plain text
Monitoring
- Horizon dashboard accessible
- Failed job alerts configured
- M-Pesa callback monitoring active
- Uptime monitoring on webhook URL
- OpenAI error rate alerting configured
Frequently Asked Questions
Can I integrate full M-Pesa functionality into a WhatsApp bot?
Yes. You can integrate all Daraja API capabilities — STK push, C2B validation and confirmation, B2C payouts, and dynamic QR code generation. Each requires separate Safaricom registration and appropriate business verification, but all are accessible to registered Kenyan businesses with a valid Daraja account.
How long does it take to build a production-ready AI WhatsApp bot in Kenya?
A minimal version (STK push + AI replies + basic memory) takes 3–7 days for an experienced Laravel developer. A full production system with C2B, B2C, QR, reconciliation, human escalation, monitoring, and security hardening typically takes 4–8 weeks, depending on complexity and existing system integrations.
Is WhatsApp Business API available in Kenya?
Yes. Meta Cloud API is available in Kenya and fully supports Kenyan phone numbers. Businesses can apply directly through the Meta Business Manager. Third-party providers like Twilio and Vonage also offer WhatsApp API access with local support options.
What does Safaricom require to access Daraja B2C?
For B2C, you need a registered business entity in Kenya, a verified M-Pesa Paybill shortcode, a completed Daraja developer account, and an application submitted through the Daraja portal specifying your use case. B2C approval typically takes 5–15 business days.
How much does it cost to run an AI WhatsApp bot in Kenya per month?
For a business handling ~10,000 conversations per month, total operating costs typically range from KES 35,000 to KES 55,000, covering WhatsApp API fees, OpenAI tokens, and hosting — compared to KES 180,000–250,000 for an equivalent human support team.
Can the bot handle Swahili and Sheng?
Yes, when using GPT-4o or Claude. Both models have strong Swahili capability. Sheng requires explicit instruction in your system prompt and testing with real Kenyan users. Always include Swahili test cases in your QA process before going live.
What happens if the OpenAI API goes down?
Always implement a fallback. If the OpenAI API returns an error, catch the exception and send a graceful message: "Samahani, mfumo wetu una tatizo. Jaribu tena baadaye au piga simu [number]." Log the failure and alert your team.
Do I need to comply with the Kenya Data Protection Act for a WhatsApp bot?
Yes. Any system that collects, stores, or processes personal data of Kenyan residents is subject to the Kenya Data Protection Act (2019) and its regulations. This includes obtaining explicit consent, providing a privacy notice, securing stored data, and enabling data deletion requests from customers.
Final Thoughts
In 2026, the competitive advantage for Kenyan businesses is not simply having a WhatsApp bot — most competitors already have basic automation. The advantage belongs to businesses that have built payment-integrated AI agents that handle real business logic end-to-end.
The architecture in this guide — agentic reasoning, tool calling, M-Pesa C2B/B2C/QR, robust reconciliation, and proper security — is the foundation of that advantage.
The businesses winning in Kenyan e-commerce, fintech, and service industries are those who invested early in reliable AI reasoning (not keyword matching), full M-Pesa integration (not just STK push), secure and auditable architecture especially for financial transactions, and Swahili-aware systems that meet customers in their language.
If you are evaluating whether to build this in-house or with a development partner, consider the complexity of B2C security credential management, Daraja compliance requirements, and the ongoing maintenance of AI prompt engineering. These are not trivial engineering challenges.
This guide is maintained by the Statum engineering team. For questions about production implementation, M-Pesa integration, or AI-powered automation for your Kenyan business, visit statum.co.ke.
Related reading: