USSD Plus Paystack: Reaching Feature Phone Users in Kenya
Paystack does not support USSD payments in Kenya directly. To reach feature phone users, build a USSD menu using Africa's Talking or a similar USSD provider as your frontend. When the user selects a product or service through the USSD menu, generate a Paystack payment link on your server and deliver it via SMS. The user opens the link on a basic browser or receives an M-Pesa STK push. For users without any data access, collect their phone number through USSD and initiate the Paystack Charge API with the mobile_money channel, which triggers an STK push to their M-Pesa phone.
The Feature Phone Reality in Kenya
Kenya has over 60 million mobile subscriptions, but a significant portion of those are on feature phones. These are the Nokia 105s, the Tecno T301s, and the itel it2180s that dominate rural markets and budget-conscious urban users. Feature phones cannot run apps, cannot open modern web pages, and cannot render JavaScript-driven checkout flows.
But feature phones can do two things that matter: navigate USSD menus and receive SMS messages. Every M-Pesa transaction in Kenya started on USSD. The *334# or *150# shortcode menus are second nature to tens of millions of Kenyans. If your product serves rural areas, agricultural communities, low-income urban populations, or anyone who has not upgraded to a smartphone, you need a USSD interface.
The problem is that Paystack's checkout is web-based. You cannot display a Paystack checkout page on a feature phone. And Paystack does not offer USSD as a direct payment channel in Kenya (they do in Nigeria, but the Kenya integration works differently).
The solution is a hybrid architecture: USSD for the user interface, your server for the business logic, and Paystack for payment processing. The payment itself happens through M-Pesa (which works on every phone) or through an SMS payment link (for users with basic internet).
The Hybrid Architecture: USSD + Server + Paystack
The system has three layers that work together.
Layer 1: USSD provider. Africa's Talking, Hover, or another USSD gateway provides the shortcode (e.g., *384*123#) and routes USSD sessions to your server. When a user dials the shortcode, the provider sends a request to your webhook URL with the user's input. You respond with menu text, and the provider displays it on the user's phone. This back-and-forth continues until the session ends.
Layer 2: Your server. Your Express or Django server handles the USSD session logic: menu navigation, product selection, quantity, and customer details. When the user confirms their order, your server creates a Paystack transaction and either sends an SMS with the payment link or initiates an M-Pesa STK push through the Paystack Charge API.
Layer 3: Paystack. Processes the actual payment. Sends webhooks when the payment succeeds or fails. Settles funds to your account.
// The flow
// 1. User dials *384*123#
// 2. Africa's Talking sends POST to your server
// 3. Your server responds with menu text
// 4. User navigates menu (product > quantity > confirm)
// 5. Your server creates Paystack transaction
// 6. Option A: Send SMS with payment link
// Option B: Initiate M-Pesa STK push via Paystack Charge API
// 7. User completes payment on their phone
// 8. Paystack sends charge.success webhook
// 9. Your server fulfils the order (SMS confirmation, delivery, etc.)
This architecture keeps each layer doing what it is best at. USSD handles the interface on devices that cannot do anything else. Your server handles business logic and session management. Paystack handles payment security, compliance, and settlement.
Triggering M-Pesa Payment via Paystack Charge API
The most seamless approach for feature phone users: after they confirm their order on the USSD menu, send an M-Pesa STK push to their phone. No SMS link needed, no browser needed. The STK push appears on their phone, they enter their M-Pesa PIN, and the payment is done.
async function initiatePaymentForUSSD(sessionId, phoneNumber, product) {
const reference = `ussd_${sessionId}_${Date.now()}`;
// Create order record
await db.query(
`INSERT INTO orders (phone, product_id, product_name, amount, paystack_reference, status, source)
VALUES ($1, $2, $3, $4, $5, 'pending', 'ussd')`,
[phoneNumber, product.id, product.name, product.price, reference]
);
// Use Paystack Charge API with mobile_money channel
const response = await axios.post(
'https://api.paystack.co/charge',
{
email: `${phoneNumber.replace('+', '')}@ussd.yourapp.com`,
amount: product.price,
currency: 'KES',
mobile_money: {
phone: phoneNumber,
provider: 'mpesa',
},
reference: reference,
metadata: {
source: 'ussd',
session_id: sessionId,
product_id: product.id,
},
},
{ headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` } }
);
return response.data;
}
The timing matters. The USSD session and the STK push are independent. The USSD session ends when you send the "END" response. The STK push arrives a few seconds later. The user sees: the USSD session closes, then the M-Pesa prompt appears. This handoff feels natural because Kenyans are used to USSD-to-M-Pesa flows from Safaricom's own services.
Handling the response. The Paystack Charge API may return a "pending" status, meaning the STK push has been sent but the user has not entered their PIN yet. Do not treat pending as failure. Listen for the charge.success webhook to confirm payment. If the user does not complete the STK push (they ignore it or enter wrong PIN), the transaction expires and you receive no webhook. Handle this by marking the order as expired after a timeout (5 to 10 minutes).
// Webhook handler for USSD-initiated payments
async function handleUSSDPaymentSuccess(data) {
const reference = data.reference;
const order = await db.query(
`UPDATE orders SET status = 'paid', paid_at = NOW()
WHERE paystack_reference = $1 AND status = 'pending'
RETURNING *`,
[reference]
);
if (order.rowCount === 0) return;
const o = order.rows[0];
// Confirm via SMS (the user cannot receive in-app notifications)
await sendSMS(
o.phone,
`Payment confirmed! Your order for ${o.product_name} (KES ${o.amount / 100}) has been processed. Thank you.`
);
// Fulfil the order (activate subscription, send ticket, etc.)
await fulfilOrder(o);
}
Alternative: SMS Payment Links
If the Paystack Charge API with mobile_money is not available or you want to give the user more payment options (card, bank transfer), send them a Paystack payment link via SMS.
async function sendPaymentLinkViaSMS(phoneNumber, product, reference) {
// Initialize Paystack transaction
const response = await axios.post(
'https://api.paystack.co/transaction/initialize',
{
email: `${phoneNumber.replace('+', '')}@ussd.yourapp.com`,
amount: product.price,
reference: reference,
currency: 'KES',
channels: ['mobile_money', 'card'],
callback_url: `https://yourapp.com/orders/confirm?ref=${reference}`,
metadata: {
source: 'ussd_sms_link',
product_id: product.id,
},
},
{ headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` } }
);
const paymentUrl = response.data.data.authorization_url;
// Shorten the URL for SMS
const shortUrl = await shortenUrl(paymentUrl);
// Send SMS
await sendSMS(
phoneNumber,
`Pay for ${product.name} (KES ${product.price / 100}): ${shortUrl}`
);
}
The limitation. SMS payment links require the user to open a browser. Feature phones with basic internet (Opera Mini, KaiOS browser) can handle Paystack's checkout page, but the experience is slow and clunky. Pure feature phones without any browser cannot use this approach at all. That is why the STK push method (previous section) is preferred for the deepest feature phone reach.
URL shortening. Paystack authorization URLs are long. SMS has a 160-character limit per segment. Use a URL shortener (Bitly, your own shortener, or a redirect through your domain) to keep the SMS under one segment. Multiple SMS segments cost more and look messy on a feature phone screen.
Expiry. Paystack authorization URLs expire (typically within 24 hours). Include the expiry context in the SMS: "Pay within 24 hours" or create a more specific deadline. If the link expires before the user acts, they need to dial the USSD shortcode again to get a new link.
USSD Menu for Account Management
Beyond payments, USSD menus can handle account management tasks that keep feature phone users engaged with your service.
Check balance/subscription status. Let users dial the shortcode and select "My Account" to see their current subscription status, last payment date, and balance (if applicable). This is especially useful for subscription services where users want to know if their access is still active.
async function handleAccountCheck(phoneNumber) {
const account = await db.query(
`SELECT s.status, s.current_period_end, s.plan_name
FROM subscriptions s
JOIN users u ON s.user_id = u.id
WHERE u.phone = $1 AND s.status = 'active'
ORDER BY s.created_at DESC LIMIT 1`,
[phoneNumber]
);
if (account.rows.length === 0) {
return 'END You have no active subscription. Select option 1 from the main menu to subscribe.';
}
const sub = account.rows[0];
return `END Your subscription:\nPlan: ${sub.plan_name}\nStatus: Active\nExpires: ${formatDate(sub.current_period_end)}\nDial *384*123# to renew or change plan.`;
}
Order history. Show the user their last 3 to 5 orders with status (paid, pending, delivered). USSD screens are small (about 160 characters visible), so keep the information dense and abbreviate where necessary.
Update phone number. If a user changes their SIM but keeps the same account, let them update their registered number through USSD by verifying with a code sent to their old number. This is important for services where the phone number is tied to payment and delivery.
Help and support. Include a "Help" option that displays a support phone number or sends an SMS with instructions. Feature phone users cannot visit a help center website, so the support path must stay within SMS and phone calls.
Reaching Rural Kenyan Customers
Rural Kenya has specific challenges that affect your USSD+Paystack integration.
Network reliability. USSD sessions over Safaricom are generally stable, but in rural areas with weak signal, sessions drop more frequently. Design your menu flow so that a dropped session does not lose the user's progress. If they were mid-order when the session dropped, save the session state server-side and let them resume when they dial back.
// Save session state for resume
async function saveSessionState(phoneNumber, state) {
await db.query(
`INSERT INTO ussd_sessions (phone, state, updated_at)
VALUES ($1, $2, NOW())
ON CONFLICT (phone) DO UPDATE SET state = $2, updated_at = NOW()`,
[phoneNumber, JSON.stringify(state)]
);
}
async function getSessionState(phoneNumber) {
const result = await db.query(
`SELECT state FROM ussd_sessions
WHERE phone = $1 AND updated_at > NOW() - INTERVAL '30 minutes'`,
[phoneNumber]
);
return result.rows[0] ? JSON.parse(result.rows[0].state) : null;
}
// At the start of a new USSD session
app.post('/ussd', async (req, res) => {
const { phoneNumber, text } = req.body;
const inputs = text.split('*').filter(Boolean);
if (inputs.length === 0) {
// Check for saved session
const savedState = await getSessionState(phoneNumber);
if (savedState && savedState.productSelected) {
let response = `CON Welcome back! You were ordering ${savedState.productName}.\n`;
response += '1. Continue with this order\n';
response += '2. Start a new order';
return res.send(response);
}
// Show main menu...
}
});
Language. Consider offering USSD menus in Swahili alongside English. A menu that says "Chagua bidhaa" (Select a product) reaches more people than one that says "Select a product" only in English. Use the user's language preference if stored, or offer language selection as the first menu option.
Agent networks. In some rural areas, M-Pesa agents serve as community access points. Consider building an agent mode where an M-Pesa agent can initiate a purchase on behalf of a customer. The agent dials the USSD code, enters the customer's phone number, and the STK push goes to the customer's phone. The agent assists with the process, and the customer pays from their own M-Pesa account.
Literacy. Keep menu text simple. Short sentences. Common words. Avoid abbreviations that are not universally understood. Test your USSD copy with actual users in the target community before launching.
Testing and Deploying the USSD Integration
Africa's Talking sandbox. Africa's Talking provides a sandbox environment where you can test USSD flows without a live shortcode. Use their USSD simulator to walk through your menu logic before deploying. The simulator mimics a real USSD session, sending requests to your webhook URL and displaying your responses.
// Africa's Talking sandbox configuration
const AfricasTalking = require('africastalking')({
apiKey: process.env.AT_API_KEY,
username: process.env.AT_SANDBOX ? 'sandbox' : process.env.AT_USERNAME,
});
// Test sending SMS from sandbox
async function testSMS(phone, message) {
const sms = AfricasTalking.SMS;
const result = await sms.send({
to: [phone],
message: message,
// In sandbox, messages are simulated, not actually sent
});
return result;
}
Paystack test mode. Use Paystack test keys alongside the Africa's Talking sandbox. Test the full flow: USSD session, order creation, Paystack charge initiation, webhook handling, and SMS confirmation. Paystack test mode simulates successful and failed payments without moving real money.
Live shortcode. To go live, apply for a USSD shortcode through Africa's Talking and the Communications Authority of Kenya. Shared shortcodes (like *384*123#) are cheaper and faster to get. Dedicated shortcodes (like *123#) are expensive and take longer but are easier for users to remember. Most startups begin with shared shortcodes.
Monitoring. Track three metrics in production: USSD session completion rate (how many users who start a session reach the payment step), payment conversion rate (how many users who reach the payment step complete payment), and session drop rate (how many sessions die due to timeout or network issues). These metrics tell you where users are getting stuck and where the experience needs improvement.
For the full context on Paystack payment methods in Kenya, including M-Pesa, PesaLink, and cards, see our Paystack in Kenya hub guide.
When This Architecture Makes Sense
Not every product needs a USSD interface. This architecture is the right choice when your target audience includes a significant number of feature phone users, your product is simple enough to navigate in 3 to 4 USSD menu levels, and the payment amount justifies the development effort.
Good candidates. Agricultural input ordering (seeds, fertilizer) for smallholder farmers. Utility bill payments in areas without smartphone penetration. Subscription services for rural content (agricultural tips, market prices, weather alerts). Event ticket sales for community events. Savings group contributions. Insurance premium collection for micro-insurance products.
Not ideal for. Products that require browsing (e-commerce with many products), services that need rich media (video streaming, image-heavy content), or flows that require complex input (long forms, file uploads). If the user needs to make choices from more than 10 options, USSD becomes unwieldy and a different channel (agent network, call center) serves them better.
The smartphone transition. Smartphone penetration in Kenya is growing fast. The audience you serve via USSD today may be on smartphones in two years. Build your USSD integration as a complement to your web and mobile experiences, not as a replacement. Share the same backend, the same order system, and the same Paystack integration. The only difference is the frontend: USSD text versus a web page. When a user upgrades to a smartphone, their account and history carry over seamlessly.
Key Takeaways
- ✓Paystack does not offer a native USSD payment channel in Kenya. You need a separate USSD provider (Africa's Talking, Hover, or similar) to build the USSD menu, and Paystack handles the actual payment processing.
- ✓The architecture is: USSD menu as the user interface, your server as the bridge, and Paystack as the payment backend. The USSD session collects user intent, your server initiates the Paystack transaction, and the payment completes via SMS link or M-Pesa STK push.
- ✓SMS-delivered payment links are the simplest bridge between USSD and Paystack. The user navigates the USSD menu, your server generates a Paystack payment link, and the user receives it via SMS to complete payment.
- ✓For users with no data at all, use the Paystack Charge API with the mobile_money channel to send an M-Pesa STK push directly to their phone. No browser needed. The USSD session collects their phone number, and you initiate the charge server-side.
- ✓Keep USSD menus short. Feature phone users are used to quick interactions. Three to four menu levels maximum. Every extra step loses users.
- ✓Rural Kenya has network reliability challenges. Design for timeouts and retries. USSD sessions expire quickly (usually 3 to 5 minutes), so initiate the payment before the session ends.
Frequently Asked Questions
- Does Paystack support USSD payments directly in Kenya?
- No, Paystack does not offer USSD as a direct payment channel in Kenya. In Nigeria, Paystack supports bank USSD codes as a payment method, but this is not available in the Kenyan market. To serve USSD users in Kenya, you need a separate USSD provider (like Africa's Talking) for the menu interface and use Paystack for the actual payment processing through M-Pesa or card.
- How much does a USSD shortcode cost in Kenya?
- Costs vary by provider and shortcode type. Shared shortcodes through Africa's Talking start at lower monthly fees. Dedicated shortcodes are significantly more expensive and require approval from the Communications Authority of Kenya. Check Africa's Talking pricing page for current rates. For most startups, a shared shortcode is sufficient and much more affordable.
- Can a user complete payment without any internet access?
- Yes, if you use the Paystack Charge API to send an M-Pesa STK push. USSD does not require internet (it uses the GSM signaling channel). M-Pesa STK push also does not require internet on the user phone. The entire flow from USSD menu navigation to M-Pesa PIN entry works on a feature phone with no data connection. Only your server needs internet to communicate with Paystack and Africa's Talking APIs.
- What happens if the USSD session times out before I can send the STK push?
- The USSD session and the payment are independent. Even if the USSD session times out, the STK push (already initiated by your server) still arrives on the user phone. The user enters their PIN and the payment goes through. Your webhook receives the charge.success event and you send an SMS confirmation. The session timeout does not affect the payment. Just make sure you initiate the payment before sending the "END" response, not after.
- Can I use Africa's Talking for both USSD and SMS in this architecture?
- Yes, and it simplifies your integration. Africa's Talking provides USSD, SMS, and voice APIs under one account. Use their USSD API for the menu, their SMS API for payment link delivery and order confirmations, and Paystack for payment processing. This gives you two providers to manage (Africa's Talking and Paystack) instead of three or more.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon