Building a SaaS Pricing Page That Converts: Developer's Perspective
Technical and UX lessons from building pricing pages for Pixel Prep and other SaaS products, including Stripe integration patterns.
Pricing Pages Are Product Pages
Your pricing page is where revenue decisions happen. After building pricing pages for several SaaS products, here's what works from both a technical and UX perspective.
Lesson 1: Credit Packs Beat Subscriptions for Usage-Based Products
For Pixel Prep (AI image processing), we tested both subscriptions and credit packs. Credits won decisively:
- Lower barrier to entry — $4.99 for 50 credits vs. $19.99/month subscription
- No cancellation anxiety — credits don't auto-renew
- Natural upselling — users who exhaust credits buy larger packs
- Simpler code — no subscription lifecycle management
Implementing Credit Packs
const PRICING_TIERS = [
{
id: 'starter',
name: 'Starter',
credits: 50,
price: 499, // cents
perCredit: 0.10,
popular: false
},
{
id: 'growth',
name: 'Growth',
credits: 200,
price: 1499,
perCredit: 0.075,
popular: true // Highlighted on pricing page
},
{
id: 'business',
name: 'Business',
credits: 500,
price: 2999,
perCredit: 0.06,
popular: false
}
];
Lesson 2: Show the Per-Unit Price
Customers compare value, not total price. Always show both:
<div class="pricing-card">
<h3>Growth</h3>
<div class="price">$14.99</div>
<div class="per-unit">$0.075 per image</div>
<div class="credits">200 credits included</div>
<div class="savings">Save 25% vs Starter</div>
</div>
Lesson 3: The "Most Popular" Badge Works
Highlighting one tier as "Most Popular" or "Best Value" anchors the customer's decision. It's not manipulative — it genuinely helps people who don't want to analyze every option.
.pricing-card.popular {
border: 2px solid #4f46e5;
transform: scale(1.05);
position: relative;
}
.pricing-card.popular::before {
content: 'Most Popular';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: #4f46e5;
color: white;
padding: 4px 16px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
Lesson 4: Stripe Checkout Reduces Friction
Don't build your own payment form. Stripe Checkout is hosted by Stripe, handles 3D Secure, saves payment methods, and supports dozens of payment methods:
app.post('/api/purchase', async (req, res) => {
const tier = PRICING_TIERS.find(t => t.id === req.body.tierId);
if (!tier) return res.status(400).json({ error: 'Invalid tier' });
const session = await stripe.checkout.sessions.create({
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: `${tier.name} - ${tier.credits} Credits`,
description: `Process up to ${tier.credits} images`
},
unit_amount: tier.price
},
quantity: 1
}],
mode: 'payment',
success_url: `${BASE_URL}/dashboard?purchased=${tier.id}`,
cancel_url: `${BASE_URL}/pricing`,
metadata: {
userId: req.user.id,
tierId: tier.id,
credits: tier.credits
}
});
res.json({ checkoutUrl: session.url });
});
Lesson 5: Free Tier Drives Adoption
Give new users 5-10 free credits. They experience the product before paying:
async function createUser(email) {
const user = await db.query(
`INSERT INTO users (email, credits, created_at)
VALUES ($1, $2, NOW()) RETURNING *`,
[email, 10] // 10 free credits
);
return user.rows[0];
}
Conversion rate from free to paid was 23% — significantly higher than a free trial with a time limit.
Lesson 6: Show Remaining Credits Everywhere
Users who see their credit balance are reminded to buy more when it's low:
// Middleware that adds credits to every response
app.use(async (req, res, next) => {
if (req.user) {
const { rows } = await db.query(
'SELECT credits FROM users WHERE id = $1',
[req.user.id]
);
res.locals.credits = rows[0]?.credits || 0;
}
next();
});
Lesson 7: Handle Failed Payments Gracefully
Not every checkout succeeds. Handle the edge cases:
// Webhook: checkout.session.expired
if (event.type === 'checkout.session.expired') {
const session = event.data.object;
// Send a follow-up email with a new checkout link
await sendEmail(session.customer_email, {
subject: 'Complete your purchase',
template: 'abandoned-checkout',
data: { tierId: session.metadata.tierId }
});
}
Pricing Page Performance
The pricing page should load instantly — it's a decision point, not a place for heavy JavaScript:
- Static HTML for the pricing cards (no client-side rendering)
- No external fonts on the pricing page (use system fonts)
- Prefetch the Stripe Checkout link on hover
- Total page weight under 50KB including CSS
A/B Testing Pricing
Before committing to price points, test with real traffic:
function getPricingVariant(userId) {
// Simple hash-based A/B split
const hash = crypto.createHash('md5').update(userId).digest('hex');
const bucket = parseInt(hash.slice(0, 2), 16) % 2;
return bucket === 0 ? 'control' : 'variant';
}
We found that the $14.99 Growth pack outsold a $9.99 version by 15% in revenue — people chose higher value over lower price.
Conclusion
A great pricing page combines clear value communication with frictionless payment processing. Show per-unit prices, highlight the best value tier, use Stripe Checkout to handle payments, and give users a free taste before asking them to pay. The technical implementation is straightforward — the hard part is choosing the right pricing model for your product.
Related Project
PixelPrepAI-powered product image resizer and background remover for e-commerce sellers. Bulk resize, smart canvas fitting, and AI background removal with one click.