by InlinexDev

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.

SaaSpricingStripeconversione-commerce

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

PixelPrep

AI-powered product image resizer and background remover for e-commerce sellers. Bulk resize, smart canvas fitting, and AI background removal with one click.