Building a Digital Content Paywall for Kenyan Readers
Build a Kenyan content paywall by creating subscription plans at daily, weekly, and monthly intervals with Paystack Subscriptions. Accept M-Pesa micropayments for single-article purchases using the Initialize Transaction endpoint. Manage free tier access with article counters stored in cookies or user accounts. Use Paystack authorization codes to charge recurring subscribers. Control content delivery server-side by checking subscription status before serving protected content. Price for the Kenyan market with daily plans starting as low as KES 20 to 50.
Why Kenyan Content Creators Need Paywalls
Ad revenue in Kenya pays poorly. A Kenyan blog with 100,000 monthly visitors might earn KES 5,000 to 15,000 from Google AdSense. That is not enough to sustain a publication, pay writers, or invest in better content. The math does not work because advertisers pay less for African traffic than for North American or European traffic.
Paywalls flip the equation. Instead of monetizing attention through ads, you monetize value directly. If 1% of those 100,000 readers pay KES 100 per month, that is KES 100,000 in monthly recurring revenue. Far more than ads would ever generate from the same audience.
The challenge in Kenya is payment friction. International paywall solutions assume readers have credit cards and are comfortable with monthly billing. Most Kenyan readers pay through M-Pesa, prefer smaller amounts, and are used to daily or weekly commitments (think data bundles, not annual subscriptions). A paywall built for Kenya needs to meet readers where they are.
Paystack handles the payment layer. You build the access control, subscription management, and content delivery logic around it.
Designing Subscription Plans for the Kenyan Market
Offer at least three plan durations. Kenyan readers have different budgets and reading habits.
Daily pass (KES 20 to 50). Unlimited access for 24 hours. This is the data bundle model: small, affordable, no commitment. Perfect for readers who binge on content during their commute or lunch break. The daily plan has the highest conversion rate for first-time payers because the amount feels negligible.
Weekly pass (KES 80 to 150). Seven days of access. Targets regular readers who visit the site a few times a week. Price it at a discount relative to daily (7 daily passes would cost more than 1 weekly). This is your sweet spot for building reading habits.
Monthly subscription (KES 250 to 500). The full commitment. Priced to reward loyal readers. Include perks that daily and weekly do not get: access to archives, exclusive content, early access, or the ability to save articles offline.
// Plan definitions
const plans = [
{
name: 'Daily Pass',
code: 'daily-pass',
amount: 5000, // KES 50 in kobo/cents
interval: 'daily',
description: 'Full access for 24 hours',
},
{
name: 'Weekly Pass',
code: 'weekly-pass',
amount: 15000, // KES 150
interval: 'weekly',
description: 'Full access for 7 days',
},
{
name: 'Monthly Pro',
code: 'monthly-pro',
amount: 50000, // KES 500
interval: 'monthly',
description: 'Full access + archives + exclusive content',
},
];
// Create plans on Paystack
async function createPaystackPlan(plan) {
const response = await axios.post(
'https://api.paystack.co/plan',
{
name: plan.name,
amount: plan.amount,
interval: plan.interval,
currency: 'KES',
},
{ headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` } }
);
return response.data.data.plan_code;
}
Pricing psychology. Kenyan readers are used to Safaricom data bundle pricing. A daily bundle costs KES 50 for 1GB. If your daily content pass costs the same as a data bundle, readers see it as reasonable. Price anchoring against familiar purchases makes the paywall feel less foreign.
Pay-per-article. Alongside subscriptions, offer single-article purchases for readers who only want one piece. Price these at KES 10 to 50 depending on content depth. This is a separate Paystack transaction, not a subscription. Think of it as the newspaper stand model: buy one paper, no commitment.
Free Tier Management and the Metered Paywall
A hard paywall (everything behind a wall, no free content) kills traffic and SEO. A metered paywall (some content free, then paywall) lets readers discover your content before asking them to pay.
The metered model. Give every visitor 3 to 5 free articles per month. After they hit the limit, show the paywall. Track usage with a cookie for anonymous visitors or a counter in the user record for logged-in users.
async function checkArticleAccess(req, res, next) {
const articleId = req.params.articleId;
const userId = req.user?.id;
// Check if user has an active subscription
if (userId) {
const sub = await db.query(
`SELECT * FROM subscriptions
WHERE user_id = $1 AND status = 'active' AND expires_at > NOW()`,
[userId]
);
if (sub.rows.length > 0) {
req.hasAccess = true;
return next();
}
// Check if article was individually purchased
const purchase = await db.query(
'SELECT * FROM article_purchases WHERE user_id = $1 AND article_id = $2',
[userId, articleId]
);
if (purchase.rows.length > 0) {
req.hasAccess = true;
return next();
}
}
// Check metered access
const freeLimit = 5;
const cookieName = 'articles_read';
const articlesRead = JSON.parse(req.cookies[cookieName] || '[]');
if (articlesRead.includes(articleId)) {
// Already read this one, does not count again
req.hasAccess = true;
return next();
}
if (articlesRead.length < freeLimit) {
// Still within free tier
articlesRead.push(articleId);
res.cookie(cookieName, JSON.stringify(articlesRead), {
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
httpOnly: true,
});
req.hasAccess = true;
return next();
}
// Paywall
req.hasAccess = false;
next();
}
What to show behind the paywall. Do not show a blank page. Show the first two or three paragraphs of the article (the hook) and then fade to a paywall overlay. The reader needs to see enough value to justify paying. Include: the article title, the opening paragraphs, a clear call to action ("Continue reading for KES 30" or "Subscribe from KES 50/day"), and the M-Pesa payment button.
Defeating cookie clearing. Determined readers will clear cookies to reset their free article count. Accept this as a cost of the metered model. The readers who clear cookies were unlikely to pay anyway. The ones who convert are the readers who value the content enough to pay rather than play the cookie game. For more control, require login after the free tier is exhausted, which ties the meter to an account rather than a cookie.
Single-Article M-Pesa Micropayments
Single-article purchases are micropayments. KES 20 to 50 per article. These small transactions need to be fast and frictionless. The reader is making an impulse decision. If the payment flow takes more than 30 seconds, they will abandon.
async function purchaseSingleArticle(userId, articleId, userPhone) {
const article = await getArticle(articleId);
const price = article.single_price || 3000; // KES 30 default
const reference = `article_${articleId}_${userId}_${Date.now()}`;
// Create purchase record
await db.query(
`INSERT INTO article_purchases (user_id, article_id, amount, paystack_reference, status)
VALUES ($1, $2, $3, $4, 'pending')`,
[userId, articleId, price, reference]
);
// Initialize Paystack transaction with M-Pesa only
const response = await axios.post(
'https://api.paystack.co/transaction/initialize',
{
email: `${userPhone}@reader.yourapp.com`,
amount: price,
reference: reference,
currency: 'KES',
channels: ['mobile_money'],
callback_url: `https://yourapp.com/articles/${article.slug}?ref=${reference}`,
metadata: {
article_id: articleId,
user_id: userId,
purchase_type: 'single_article',
},
},
{ headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` } }
);
return response.data.data.authorization_url;
}
When the charge.success webhook fires, mark the purchase as complete and grant permanent access to that article for that user.
async function handleArticlePurchaseSuccess(reference) {
const verification = await verifyTransaction(reference);
if (verification.status !== 'success') return;
await db.query(
`UPDATE article_purchases SET status = 'completed'
WHERE paystack_reference = $1 AND status = 'pending'`,
[reference]
);
}
Reducing friction. For returning readers, use the Paystack Charge API with their stored authorization code to charge without redirecting to the payment page. The reader taps "Buy this article," confirms on the popup, and the STK push hits their phone immediately. No redirect, no extra page loads.
async function chargeReturningReader(email, authorizationCode, amount, articleId) {
const reference = `article_${articleId}_${Date.now()}`;
const response = await axios.post(
'https://api.paystack.co/transaction/charge_authorization',
{
email: email,
amount: amount,
currency: 'KES',
authorization_code: authorizationCode,
reference: reference,
metadata: {
article_id: articleId,
purchase_type: 'single_article_returning',
},
},
{ headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` } }
);
return response.data;
}
Authorization codes are returned in the charge.success webhook payload under data.authorization.authorization_code. Store them against the user record for future charges. Only use them with explicit user consent.
Recurring Subscriptions with Paystack
For weekly and monthly plans, use Paystack Subscriptions to handle automatic renewals. The first payment creates the subscription, and Paystack charges the customer automatically on each renewal date.
async function createSubscription(userId, planCode, userPhone) {
const email = `${userPhone}@reader.yourapp.com`;
// First, initialize a transaction on the plan
const response = await axios.post(
'https://api.paystack.co/transaction/initialize',
{
email: email,
amount: getPlanAmount(planCode),
currency: 'KES',
plan: planCode,
channels: ['mobile_money', 'card'],
callback_url: 'https://yourapp.com/subscribe/confirm',
metadata: {
user_id: userId,
subscription_type: planCode,
},
},
{ headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` } }
);
return response.data.data.authorization_url;
}
After the first successful charge, Paystack creates the subscription and sends a subscription.create webhook. Store the subscription details in your database.
async function handleSubscriptionCreated(data) {
const userId = data.metadata?.user_id;
if (!userId) return;
const nextPaymentDate = data.next_payment_date;
const planCode = data.plan.plan_code;
await db.query(
`INSERT INTO subscriptions (user_id, paystack_subscription_code, plan_code, status, current_period_end)
VALUES ($1, $2, $3, 'active', $4)
ON CONFLICT (user_id) DO UPDATE SET
paystack_subscription_code = $2,
plan_code = $3,
status = 'active',
current_period_end = $4`,
[userId, data.subscription_code, planCode, nextPaymentDate]
);
}
Handling failed renewals. When a renewal charge fails (insufficient M-Pesa balance, expired card), Paystack sends an invoice.payment_failed webhook. Do not immediately revoke access. Give the reader a grace period (24 to 48 hours) and send an SMS asking them to load their M-Pesa wallet. Paystack retries failed charges automatically, but a proactive notification improves recovery rates.
Cancellation. Let readers cancel their subscriptions through your app. Use the Paystack Disable Subscription endpoint. The reader keeps access until the current period ends (they already paid for it), then the subscription does not renew.
async function cancelSubscription(userId) {
const sub = await db.query(
'SELECT paystack_subscription_code FROM subscriptions WHERE user_id = $1 AND status = $2',
[userId, 'active']
);
if (!sub.rows[0]) throw new Error('No active subscription');
await axios.post(
'https://api.paystack.co/subscription/disable',
{
code: sub.rows[0].paystack_subscription_code,
token: sub.rows[0].email_token,
},
{ headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` } }
);
await db.query(
'UPDATE subscriptions SET status = $1 WHERE user_id = $2',
['cancelling', userId]
);
}
Server-Side Content Delivery and Access Control
The most important rule of a paywall: never send the full content to the browser and hide it with CSS or JavaScript. Any reader who opens DevTools can read your hidden content. Access control must happen on the server.
The correct pattern. Check the reader's subscription status or article purchase on the server before rendering the page. If they have access, send the full content. If they do not, send only the preview (first few paragraphs) and the paywall component.
// Next.js API route or Express handler
app.get('/api/articles/:slug', async (req, res) => {
const article = await getArticleBySlug(req.params.slug);
if (!article) return res.status(404).json({ error: 'Not found' });
const hasAccess = await checkAccess(req.user?.id, article.id, req.cookies);
if (hasAccess) {
return res.json({
title: article.title,
content: article.full_content,
paywall: false,
});
}
// Send preview only
return res.json({
title: article.title,
content: article.preview_content, // first 2-3 paragraphs
paywall: true,
plans: await getActivePlans(),
singleArticlePrice: article.single_price,
});
});
SEO considerations. Google needs to see your content to rank it. Use the "first click free" approach: if the referrer is a search engine, show the full article. If the reader comes directly or has exceeded their metered limit, show the paywall. Alternatively, use structured data to tell Google which content is paywalled (schema.org CreativeWork with isAccessibleForFree).
Caching. Paywalled content complicates caching. You cannot cache the full page at the CDN level because different readers see different versions (full content versus preview). Use cache keys that include the access level, or cache the article content separately and assemble the page dynamically based on access checks.
Pricing Strategies for the Kenyan Market
Start low and test. The biggest risk with a Kenyan paywall is pricing too high. Start with daily passes at KES 20 to 30 and see how conversion looks. You can always raise prices. Lowering prices after launch feels like desperation and confuses existing subscribers.
Bundle with mobile data. Some Kenyan publications have explored partnerships with Safaricom or Airtel to bundle content access with data plans. This is advanced but powerful: the reader buys a KES 100 data bundle that includes access to your content. The telco handles billing, and you receive a revenue share. Worth exploring once you have proven the content's value.
Student and lower-income pricing. Consider a discounted tier for students or readers in lower-income areas. Verify student status through a university email domain. This builds habit and brand loyalty in a demographic that will eventually earn more and be willing to pay full price.
Free trials. Offer 7-day free trials for monthly subscriptions. Use Paystack's trial period feature or implement it yourself by creating a subscription that starts billing after 7 days. The reader provides their payment method upfront (so you have an authorization code), but is not charged until the trial ends. This reduces the barrier to entry while ensuring that trial users are payment-ready.
Content tiering. Not all content is equal. Breaking news can be free (it drives traffic and ad revenue). In-depth analysis, investigative pieces, and exclusive features go behind the paywall. This model keeps your free content feeding the funnel while your premium content generates subscription revenue.
For the full picture on Paystack payment methods in Kenya, see our Paystack in Kenya hub guide.
Key Takeaways
- ✓Daily and weekly plans are essential for the Kenyan market. Many readers cannot commit to monthly subscriptions, but they will pay KES 20 for a day of access or KES 100 for a week.
- ✓Single-article purchases work for readers who visit occasionally. Charge KES 10 to 50 per article through Paystack with M-Pesa as the primary channel.
- ✓Free tier management is about conversion, not restriction. Let readers see 3 to 5 free articles per month, then show a paywall with a clear value proposition and an M-Pesa payment button.
- ✓Server-side access control is mandatory. Never rely on frontend-only paywalls that can be bypassed by disabling JavaScript or inspecting the page source.
- ✓Use Paystack Subscriptions for recurring plans and the Initialize Transaction endpoint for single-article purchases. Two different payment patterns, same Paystack account.
- ✓Authorization codes let you charge returning subscribers without making them enter their payment details again. Store them securely after the first successful payment.
Frequently Asked Questions
- Can Paystack handle KES 20 micropayments efficiently?
- Paystack does process small amounts, but check their minimum transaction amount for Kenya, which may be higher than KES 20. Also consider that Paystack charges a percentage fee plus a flat fee per transaction. On a KES 20 payment, the fees may eat a significant portion of the revenue. If micropayments are your core model, calculate the unit economics carefully and consider raising the minimum single-article price to KES 30 to 50 to maintain healthy margins.
- How do I handle readers who share their login credentials?
- Track concurrent sessions. If a subscriber is logged in from two different devices or IP addresses simultaneously, flag the account. You can allow 2 to 3 concurrent sessions (phone, laptop, tablet) and block additional ones. Send an email or SMS when a new device logs in, similar to how Netflix handles account sharing. Do not punish readers for switching devices, but do prevent widespread credential sharing.
- Should I use Paystack Subscriptions or build my own recurring billing?
- Use Paystack Subscriptions for standard weekly and monthly plans. It handles retry logic, plan management, and renewal timing automatically. Build your own billing only if you need unusual intervals (like a 3-day pass) or complex plan structures (metered billing based on articles read). For most Kenyan publishers, Paystack Subscriptions cover all the common patterns.
- What conversion rate should I expect from a Kenyan metered paywall?
- International benchmarks put metered paywall conversion at 1% to 3% of regular readers. In Kenya, expect the lower end (0.5% to 1.5%) initially, improving as M-Pesa payment friction decreases and readers get used to paying for digital content. The key factor is content quality. Commodity content (news you can get elsewhere for free) converts poorly. Unique analysis, local data, and exclusive stories convert much better.
- How do I handle the daily pass expiry?
- Store the purchase timestamp and calculate expiry as purchase_time + 24 hours. Check access against this timestamp on every page load. When the 24 hours pass, the access check fails and the paywall appears again. Send an SMS 2 hours before expiry: "Your daily pass expires soon. Renew for KES 50 or upgrade to weekly for KES 150." This prompt drives renewals at the moment the reader is most likely to act.
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