← Back to articles

How to Add Payments to Your SaaS (2026 Guide)

Payment integration is where most SaaS builders lose weeks. Not because it's hard, but because there are too many choices and too many edge cases nobody warns you about. This guide gets you from zero to collecting payments with the least pain.

Choose Your Approach

ApproachComplexityTax handlingBest for
Stripe (direct)Medium-HighYou handle itFull control, US/EU focused
Lemon SqueezyLowHandled (MoR)Global sales, simple billing
PaddleLowHandled (MoR)B2B SaaS, enterprise
PolarLowHandled (MoR)Developer products

Merchant of Record vs Direct Payment Processing

Direct (Stripe, PayPal): You're the seller. You handle sales tax, VAT, refunds, and compliance.

Merchant of Record (Lemon Squeezy, Paddle, Polar): They're the legal seller. They handle taxes, compliance, refunds. You get payouts.

Our recommendation: If you're selling globally and don't want to deal with tax compliance in 130+ countries, use a Merchant of Record. If you need full control and are US/EU-only, go direct with Stripe.

Option 1: Stripe (Direct Integration)

Setup Steps

Step 1: Create products and prices

// Create product + price in Stripe dashboard or via API
const product = await stripe.products.create({
  name: 'Pro Plan',
  description: 'Full access to all features',
});

const price = await stripe.prices.create({
  product: product.id,
  unit_amount: 2900, // $29.00
  currency: 'usd',
  recurring: { interval: 'month' },
});

Step 2: Create a Checkout Session

// When user clicks "Subscribe"
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: 'price_xxxxx', quantity: 1 }],
  success_url: 'https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}',
  cancel_url: 'https://yourapp.com/pricing',
  customer_email: user.email,
  metadata: { userId: user.id }, // Critical: link to your user
});

// Redirect user to session.url

Step 3: Handle webhooks (the important part)

// POST /api/webhooks/stripe
app.post('/api/webhooks/stripe', async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body,
    req.headers['stripe-signature'],
    process.env.STRIPE_WEBHOOK_SECRET
  );

  switch (event.type) {
    case 'checkout.session.completed':
      // Activate subscription for user
      const session = event.data.object;
      await db.users.update({
        where: { id: session.metadata.userId },
        data: {
          stripeCustomerId: session.customer,
          subscriptionId: session.subscription,
          plan: 'pro',
          subscriptionStatus: 'active',
        },
      });
      break;

    case 'invoice.payment_succeeded':
      // Renewal succeeded — keep access active
      break;

    case 'invoice.payment_failed':
      // Payment failed — notify user, maybe downgrade
      break;

    case 'customer.subscription.deleted':
      // Subscription canceled — remove access
      await db.users.update({
        where: { stripeCustomerId: event.data.object.customer },
        data: { plan: 'free', subscriptionStatus: 'canceled' },
      });
      break;
  }

  res.json({ received: true });
});

Step 4: Customer portal (self-service)

// Let users manage their subscription
const portalSession = await stripe.billingPortal.sessions.create({
  customer: user.stripeCustomerId,
  return_url: 'https://yourapp.com/settings',
});
// Redirect to portalSession.url

Stripe Tax Handling

If using Stripe directly, you need to handle sales tax. Options:

  • Stripe Tax: Automatic tax calculation ($0.50/transaction). Easiest option.
  • TaxJar/Avalara: Third-party tax calculation services.
  • Manual: Only viable if selling exclusively in one jurisdiction.

Option 2: Lemon Squeezy (Merchant of Record)

Setup Steps

Step 1: Create products in Lemon Squeezy dashboard

Set up your plans, pricing, and trial periods in the dashboard. No code needed.

Step 2: Integrate checkout

// Embed checkout or redirect
const checkoutUrl = `https://yourstore.lemonsqueezy.com/checkout/buy/${variantId}?checkout[custom][user_id]=${userId}`;

// Or use the JavaScript SDK
LemonSqueezy.Setup({ apiKey: process.env.LS_API_KEY });
const checkout = await LemonSqueezy.createCheckout({
  storeId: STORE_ID,
  variantId: VARIANT_ID,
  checkoutData: {
    custom: { user_id: userId },
  },
});

Step 3: Handle webhooks

// POST /api/webhooks/lemonsqueezy
app.post('/api/webhooks/lemonsqueezy', async (req, res) => {
  // Verify webhook signature
  const event = req.body;

  switch (event.meta.event_name) {
    case 'subscription_created':
      await db.users.update({
        where: { id: event.meta.custom_data.user_id },
        data: {
          plan: 'pro',
          lsCustomerId: event.data.attributes.customer_id,
          lsSubscriptionId: event.data.id,
          subscriptionStatus: 'active',
        },
      });
      break;

    case 'subscription_cancelled':
      // Access continues until period ends
      break;

    case 'subscription_expired':
      await db.users.update({
        where: { lsSubscriptionId: event.data.id },
        data: { plan: 'free', subscriptionStatus: 'expired' },
      });
      break;
  }

  res.json({ received: true });
});

That's it. No tax configuration. No compliance worries. Lemon Squeezy handles everything as the merchant of record.

Pricing Models

Flat-Rate Monthly/Annual

The simplest model. $29/month or $290/year (give 2 months free for annual).

Free → Pro ($29/mo) → Enterprise (custom)

Best for: Most early-stage SaaS. Start here.

Per-Seat Pricing

Charge per user. Common for B2B tools.

$10/user/month, minimum 1 user

Implementation tip: Track seats in your database. Use Stripe's quantity parameter on subscriptions.

Usage-Based

Charge based on consumption (API calls, storage, compute).

$0.01/API call or $5/1000 credits

Implementation: Track usage in your app, report to Stripe via metered billing, or use Lemon Squeezy's usage-based plans.

Credits/Tokens

Sell prepaid credits that users consume.

100 credits = $10. Each action costs 1-10 credits.

Best for: AI products where cost per operation varies. Simplifies pricing psychology.

Common Mistakes

1. Not Handling Failed Payments

30-40% of subscription churn is "involuntary" — failed credit cards, expired cards, insufficient funds. Build a dunning flow:

  • Day 0: Payment fails → email user
  • Day 3: Retry payment → second email
  • Day 7: Retry → warning: access will be restricted
  • Day 14: Downgrade to free tier

Stripe handles retries automatically (Smart Retries), but you need to handle the user communication and access changes.

2. Not Testing Webhooks

Test webhooks thoroughly. Use Stripe CLI (stripe listen --forward-to localhost:3000/api/webhooks) during development. Test every event type:

  • Successful payment
  • Failed payment
  • Subscription canceled
  • Subscription upgraded/downgraded
  • Refund issued
  • Disputed charge

3. Not Linking Stripe Customer to Your User

Always pass your userId in checkout metadata. Without this link, you can't grant access when the webhook fires.

4. Checking Subscription Status Client-Side

Never check subscription status via client-side API calls. Store subscription status in your database, update via webhooks, and check your database.

5. Forgetting Annual Billing

Always offer annual billing at a discount (typically 2 months free). 30-40% of customers prefer annual. Annual billing reduces churn and improves cash flow.

6. No Free Tier or Trial

Don't ask users to pay before they experience value. Options:

  • Free tier: Limited features, unlimited time
  • Free trial: Full features, limited time (7 or 14 days)
  • Freemium: Core product free, premium features paid

Security Checklist

  • Stripe API keys in environment variables (never in code)
  • Webhook signature verification on every webhook endpoint
  • HTTPS everywhere (Stripe requires it)
  • Server-side subscription checks (never trust client-side)
  • PCI compliance via Stripe Checkout (don't handle cards directly)
  • Idempotent webhook handling (webhooks can fire multiple times)

FAQ

Stripe or Lemon Squeezy?

Stripe if you want full control and are comfortable handling tax compliance. Lemon Squeezy if you want to ship fast and sell globally without worrying about taxes. Most indie hackers should start with Lemon Squeezy.

How much does payment processing cost?

Stripe: 2.9% + $0.30 per transaction. Lemon Squeezy: 5% + payment processing (~8% total). Paddle: 5% + processing. The MoR premium is the cost of tax compliance handled for you.

Should I build billing myself or use a library?

Use existing solutions. Libraries like @lemonsqueezy/lemonsqueezy.js, stripe npm package, or full billing stacks like Tier, Lago, or Orb for usage-based billing.

When should I add payments?

After you've validated that people want your product (sign-ups, waitlist, or usage of free tier). Don't build billing before you have users.

How do I handle refunds?

Process refunds through Stripe/Lemon Squeezy dashboard or API. Update user access accordingly via webhooks. Have a clear refund policy on your site.

The Bottom Line

For most SaaS builders in 2026:

  1. Start with Lemon Squeezy or Polar for simplicity and global tax handling
  2. Migrate to Stripe when you need more control or want lower fees at scale
  3. Keep billing simple — flat-rate monthly + annual discount
  4. Handle webhooks properly — this is where 90% of billing bugs live
  5. Test everything in test mode before going live

Ship payments in a weekend, not a month. The tools are good enough in 2026 that billing shouldn't be what slows you down.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.