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.
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.
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.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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 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.
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]
);
}
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.
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.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course