Free Trials with Paystack Subscriptions
Paystack has no native trial feature. To implement free trials, manage the trial period in your application and create the Paystack subscription only when the trial ends. For card-first trials, collect card details during signup via a small verification charge, store the reusable authorization, then create the subscription when the trial expires. For no-card trials, let users sign up without payment and prompt them to add a card before the trial ends.
Why Paystack Has No Trial Field
Some payment platforms let you set a trial_days parameter on a plan or subscription. Paystack does not. You manage trial logic in your own application.
This is not a limitation in practice. Trial behavior is a business decision: how long the trial lasts, what features are available during the trial, whether a card is required, what happens at expiry. These decisions belong in your code, not in your payment provider's API.
The three common patterns:
- No-card trial: User signs up without payment. Trial period managed entirely in your app. Payment collected at the end.
- Card-first trial: User provides card details during signup (small verification charge). Trial managed in your app. Subscription created automatically at trial end.
- Start-date offset: Create the Paystack subscription immediately but set the start date in the future. The first charge happens after the trial period.
This article is part of the subscriptions and recurring billing guide.
Pattern 1: No-Card Trial
The user signs up without entering payment details. You track the trial in your database. When the trial ends, prompt them to subscribe.
// During signup
async function startFreeTrial(userId) {
var trialDays = 14;
var trialEnd = new Date();
trialEnd.setDate(trialEnd.getDate() + trialDays);
await db.query(
'INSERT INTO trials (user_id, status, started_at, expires_at) VALUES ($1, $2, NOW(), $3)',
[userId, 'active', trialEnd]
);
return { trialEnd: trialEnd, daysRemaining: trialDays };
}
// Middleware to check trial status
async function trialMiddleware(req, res, next) {
var userId = req.user.id;
// Check for active subscription first
var subscription = await getActiveSubscription(userId);
if (subscription) {
req.accessLevel = 'paid';
return next();
}
// Check trial
var trial = await db.query(
'SELECT status, expires_at FROM trials WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1',
[userId]
);
if (trial.rows.length === 0) {
req.accessLevel = 'no_trial';
return next();
}
var row = trial.rows[0];
if (row.status === 'active' && new Date(row.expires_at) > new Date()) {
var daysLeft = Math.ceil((new Date(row.expires_at) - new Date()) / (24 * 60 * 60 * 1000));
req.accessLevel = 'trial';
req.trialDaysLeft = daysLeft;
return next();
}
req.accessLevel = 'trial_expired';
next();
}
Pros: Zero signup friction. No payment barrier. Good for products where the value proposition needs to be experienced before paying.
Cons: Lower conversion rates. Users who never entered payment details have more steps between them and paying. You have no authorization code to charge when the trial ends.
Pattern 2: Card-First Trial
Collect card details during signup with a small verification charge. Store the authorization code. When the trial ends, create the subscription using the stored authorization.
// Step 1: Initialize a verification charge during signup
async function startCardFirstTrial(email) {
var response = await axios.post(
'https://api.paystack.co/transaction/initialize',
{
email: email,
amount: 5000, // 50 NGN verification charge
callback_url: 'https://yoursite.com/trial-started',
metadata: {
purpose: 'trial_card_collection',
trial_days: 14,
},
},
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
}
);
return response.data.data.authorization_url;
}
// Step 2: Handle the callback and start trial
async function handleTrialCardCallback(reference) {
var verification = await verifyTransaction(reference);
if (verification.status !== 'success') {
return { success: false, message: 'Payment verification failed' };
}
if (!verification.authorization.reusable) {
return { success: false, message: 'Please use a card payment to start your trial' };
}
var userId = await getUserByEmail(verification.customer.email);
// Store the authorization for later use
await saveAuthorization(userId, verification.authorization);
// Start the trial
var trialDays = 14;
var trialEnd = new Date();
trialEnd.setDate(trialEnd.getDate() + trialDays);
await db.query(
'INSERT INTO trials (user_id, status, started_at, expires_at, has_card) VALUES ($1, $2, NOW(), $3, $4)',
[userId, 'active', trialEnd, true]
);
return { success: true, trialEnd: trialEnd };
}
// Step 3: Convert trial to subscription when trial ends
async function convertTrialToSubscription(userId) {
var paymentMethod = await getDefaultPaymentMethod(userId);
var user = await getUser(userId);
var response = await axios.post(
'https://api.paystack.co/subscription',
{
customer: user.email,
plan: process.env.PAYSTACK_PRO_PLAN_CODE,
authorization: paymentMethod.authorization_code,
},
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
}
);
var subscription = response.data.data;
await db.query(
'UPDATE trials SET status = $1, converted_at = NOW() WHERE user_id = $2 AND status = $3',
['converted', userId, 'active']
);
return subscription;
}
Pros: Higher conversion rates. The card is already on file. The transition from trial to paid is seamless, even automatic.
Cons: Higher signup friction. Some customers abandon when asked for a card upfront. In African markets where card trust is still building, this friction can be significant.
Automating Trial Expiry and Conversion
Run a daily job that checks for expiring trials and handles the transition.
async function processExpiringTrials() {
// Trials expiring today
var expiring = await db.query(
'SELECT t.user_id, t.has_card, u.email FROM trials t JOIN users u ON t.user_id = u.id WHERE t.status = $1 AND t.expires_at::date = CURRENT_DATE',
['active']
);
for (var i = 0; i < expiring.rows.length; i++) {
var trial = expiring.rows[i];
if (trial.has_card) {
// Auto-convert to paid subscription
try {
await convertTrialToSubscription(trial.user_id);
await sendTrialConvertedEmail(trial.email);
console.log('Converted trial for user ' + trial.user_id);
} catch (error) {
console.log('Failed to convert trial for user ' + trial.user_id + ': ' + error.message);
await sendTrialConversionFailedEmail(trial.email);
}
} else {
// No card. Expire the trial.
await db.query(
'UPDATE trials SET status = $1 WHERE user_id = $2 AND status = $3',
['expired', trial.user_id, 'active']
);
await sendTrialExpiredEmail(trial.email);
}
}
// Send warnings for trials expiring in 3 days
var warningTrials = await db.query(
'SELECT t.user_id, t.has_card, u.email FROM trials t JOIN users u ON t.user_id = u.id WHERE t.status = $1 AND t.expires_at::date = CURRENT_DATE + INTERVAL '3 days'',
['active']
);
for (var j = 0; j < warningTrials.rows.length; j++) {
var warning = warningTrials.rows[j];
await sendTrialExpiringWarning(warning.email, 3, warning.has_card);
}
}
Send three notifications during the trial:
- Day 1: Welcome email with tips to get the most from the trial.
- 3 days before expiry: Reminder that the trial is ending. If no card, prompt them to add one.
- Expiry day: Trial has ended. If card-first, notify that billing has started. If no-card, prompt them to subscribe.
Tracking Trial Metrics
The metrics that matter for trials:
async function getTrialMetrics(startDate, endDate) {
var metrics = {};
// Total trials started
var started = await db.query(
'SELECT COUNT(*) as count FROM trials WHERE started_at BETWEEN $1 AND $2',
[startDate, endDate]
);
metrics.trialsStarted = parseInt(started.rows[0].count);
// Converted to paid
var converted = await db.query(
'SELECT COUNT(*) as count FROM trials WHERE started_at BETWEEN $1 AND $2 AND status = $3',
[startDate, endDate, 'converted']
);
metrics.trialsConverted = parseInt(converted.rows[0].count);
// Conversion rate
metrics.conversionRate = metrics.trialsStarted > 0
? (metrics.trialsConverted / metrics.trialsStarted * 100).toFixed(1) + '%'
: '0%';
// Card-first vs no-card breakdown
var cardFirst = await db.query(
'SELECT has_card, COUNT(*) as total, COUNT(CASE WHEN status = $1 THEN 1 END) as converted FROM trials WHERE started_at BETWEEN $2 AND $3 GROUP BY has_card',
['converted', startDate, endDate]
);
metrics.breakdown = cardFirst.rows.map(function(row) {
return {
type: row.has_card ? 'card_first' : 'no_card',
total: parseInt(row.total),
converted: parseInt(row.converted),
rate: (parseInt(row.converted) / parseInt(row.total) * 100).toFixed(1) + '%',
};
});
return metrics;
}
Typical benchmarks: card-first trials convert at 40-60%. No-card trials convert at 5-15%. These numbers vary widely by product and market. Track them for your own product and optimize accordingly.
In African SaaS markets, the gap between card-first and no-card conversion can be even wider because adding a card after the fact requires more effort and trust than in markets where saved cards are ubiquitous.
Extending and Restarting Trials
Sometimes you want to give a customer more trial time. Maybe they were on holiday, or your product had an outage during their trial, or your sales team wants to extend an enterprise prospect's evaluation.
async function extendTrial(userId, additionalDays, reason) {
var trial = await db.query(
'SELECT id, expires_at FROM trials WHERE user_id = $1 AND status = $2',
[userId, 'active']
);
if (trial.rows.length === 0) {
throw new Error('No active trial found for user ' + userId);
}
var newExpiry = new Date(trial.rows[0].expires_at);
newExpiry.setDate(newExpiry.getDate() + additionalDays);
await db.query(
'UPDATE trials SET expires_at = $1 WHERE id = $2',
[newExpiry, trial.rows[0].id]
);
// Log the extension
await db.query(
'INSERT INTO trial_extensions (trial_id, user_id, days_added, reason, created_at) VALUES ($1, $2, $3, $4, NOW())',
[trial.rows[0].id, userId, additionalDays, reason]
);
return { newExpiry: newExpiry };
}
// Restarting an expired trial (use sparingly)
async function restartTrial(userId, trialDays, reason) {
// Expire the old trial
await db.query(
'UPDATE trials SET status = $1 WHERE user_id = $2 AND status IN ($3, $4)',
['superseded', userId, 'active', 'expired']
);
// Create a new one
var trialEnd = new Date();
trialEnd.setDate(trialEnd.getDate() + trialDays);
await db.query(
'INSERT INTO trials (user_id, status, started_at, expires_at, has_card, note) VALUES ($1, $2, NOW(), $3, $4, $5)',
[userId, 'active', trialEnd, false, 'Restarted: ' + reason]
);
return { trialEnd: trialEnd };
}
Track trial extensions so you can audit them. If one sales rep is extending every prospect to 90 days, that is a signal worth investigating.
Trial Best Practices for African Markets
Trials in African SaaS markets have specific dynamics:
Keep trials short. 7-14 days is enough. Longer trials (30 days) see lower conversion rates because the urgency to decide fades. If the customer cannot evaluate your product in two weeks, the problem is your onboarding, not the trial length.
Consider the card trust factor. Many African internet users are cautious about entering card details online. If you require a card for the trial, explain clearly that the verification charge is small and refundable, and that they can cancel anytime.
Make the trial valuable immediately. In markets where SaaS awareness is still growing, the trial needs to deliver a tangible win quickly. A developer should deploy something. A business owner should see a report. An educator should create a lesson. First-day value drives conversion.
Send WhatsApp or SMS reminders, not just email. Email open rates in some African markets are lower than in North America. If your customer base is more responsive to WhatsApp or SMS, use those channels for trial expiry reminders.
Offer a discount on first payment. When the trial expires, offer 20-30% off the first month. This reduces the perceived risk of committing to a paid plan. See coupons and discounts on Paystack subscriptions for implementation.
Key Takeaways
- ✓Paystack does not have a built-in trial period parameter. You manage trials in your own application logic.
- ✓Card-first trials collect payment details during signup, making conversion smoother. No-card trials reduce signup friction but have lower conversion rates.
- ✓For card-first trials, use a small verification charge to collect the authorization code, then create the subscription when the trial ends.
- ✓Track trial expiry dates in your database and run a daily job to convert or expire trials.
- ✓Trial-to-paid conversion is the critical metric. Monitor it and optimize your trial experience around it.
- ✓In African markets where card penetration is lower, no-card trials may get more signups, but card-first trials convert better.
Frequently Asked Questions
- Does Paystack have a built-in trial period feature?
- No. Paystack does not have a trial_days or trial_period parameter on plans or subscriptions. You manage the trial entirely in your application: track trial start and end dates, control access based on trial status, and create the Paystack subscription only when the trial ends and the customer is ready to pay.
- Should I require a card for the free trial?
- It depends on your market and product. Card-first trials have higher conversion rates (40-60%) but lower signup rates. No-card trials have higher signups but lower conversion (5-15%). In African markets where card trust is still building, test both approaches and measure which yields more paying customers overall.
- How do I refund the verification charge from a card-first trial?
- You can refund the verification charge via the Paystack API or Dashboard. Some companies keep the charge small enough (50 NGN) that refunding is not worth the administrative cost. Others charge zero by collecting card details through a different mechanism. Check Paystack's current policies on minimum charge amounts.
- What if the customer's card expires during the trial?
- If the card expires before the trial ends, the authorization code becomes unusable. When you try to create the subscription at trial end, it will fail. Prompt the customer to update their card before the trial expires, especially if their card expiry is within the trial window.
- Can I offer different trial lengths for different plans?
- Yes. Since trials are managed in your application, you have full control. Offer 7 days for the basic plan and 14 days for the pro plan. Or offer extended trials for enterprise prospects. Store the trial duration per plan in your configuration.
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