Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt

Use this file to discover all available pages before exploring further.

Stripe Billing Integration

LaunchMyStore uses Stripe to process app payments. This guide covers setting up Stripe, handling subscriptions, and managing the billing lifecycle.

Prerequisites

  1. A Stripe account
  2. Your app registered in the LaunchMyStore developer portal
  3. Stripe API keys (test and live)

Setup

1. Connect Stripe Account

In the developer portal, connect your Stripe account:
  1. Go to Apps > Your App > Billing
  2. Click Connect Stripe Account
  3. Complete the Stripe Connect onboarding
  4. Copy your Stripe webhook signing secret

2. Configure Webhook Endpoint

Set up a webhook endpoint to receive Stripe events:
// pages/api/webhooks/stripe.js
import Stripe from 'stripe';
import { buffer } from 'micro';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

export const config = {
  api: {
    bodyParser: false
  }
};

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).end();
  }
  
  const buf = await buffer(req);
  const sig = req.headers['stripe-signature'];
  
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle the event
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object);
      break;
    case 'customer.subscription.created':
      await handleSubscriptionCreated(event.data.object);
      break;
    case 'customer.subscription.updated':
      await handleSubscriptionUpdated(event.data.object);
      break;
    case 'customer.subscription.deleted':
      await handleSubscriptionCanceled(event.data.object);
      break;
    case 'invoice.paid':
      await handleInvoicePaid(event.data.object);
      break;
    case 'invoice.payment_failed':
      await handlePaymentFailed(event.data.object);
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
  
  res.json({ received: true });
}

3. Create Products in Stripe

Create products and prices matching your pricing model:
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

async function createProducts() {
  // Create the product
  const product = await stripe.products.create({
    name: 'My App Pro',
    description: 'Pro subscription for My App'
  });
  
  // Create monthly price
  const monthlyPrice = await stripe.prices.create({
    product: product.id,
    unit_amount: 2999,  // $29.99
    currency: 'usd',
    recurring: {
      interval: 'month'
    },
    metadata: {
      plan_id: 'pro_monthly'
    }
  });
  
  // Create annual price
  const annualPrice = await stripe.prices.create({
    product: product.id,
    unit_amount: 29990,  // $299.90 (2 months free)
    currency: 'usd',
    recurring: {
      interval: 'year'
    },
    metadata: {
      plan_id: 'pro_annual'
    }
  });
  
  return { product, monthlyPrice, annualPrice };
}

Checkout Flow

Create Checkout Session

When a merchant selects a plan, create a checkout session:
// pages/api/billing/create-checkout.js
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export default async function handler(req, res) {
  const { shopId, planId, shopDomain } = req.body;
  
  // Get or create Stripe customer
  let customer = await getStripeCustomer(shopId);
  
  if (!customer) {
    customer = await stripe.customers.create({
      metadata: {
        shop_id: shopId,
        shop_domain: shopDomain
      }
    });
    await saveStripeCustomerId(shopId, customer.id);
  }
  
  // Get price ID for the selected plan
  const priceId = getPriceIdForPlan(planId);
  
  // Create checkout session
  const session = await stripe.checkout.sessions.create({
    customer: customer.id,
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [
      {
        price: priceId,
        quantity: 1
      }
    ],
    subscription_data: {
      trial_period_days: 14,  // Optional trial
      metadata: {
        shop_id: shopId,
        plan_id: planId
      }
    },
    success_url: `https://your-app.com/billing/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `https://your-app.com/billing/canceled`,
    metadata: {
      shop_id: shopId,
      plan_id: planId
    }
  });
  
  res.json({ checkoutUrl: session.url });
}

Handle Checkout Completion

async function handleCheckoutComplete(session) {
  const { shop_id, plan_id } = session.metadata;
  const subscriptionId = session.subscription;
  
  // Fetch subscription details
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);
  
  // Update your database
  await updateShopSubscription(shop_id, {
    status: 'active',
    planId: plan_id,
    stripeSubscriptionId: subscriptionId,
    stripeCustomerId: session.customer,
    currentPeriodEnd: new Date(subscription.current_period_end * 1000),
    trialEnd: subscription.trial_end 
      ? new Date(subscription.trial_end * 1000) 
      : null
  });
  
  // Notify LaunchMyStore
  await notifyLaunchMyStore('subscription.created', {
    shopId: shop_id,
    planId: plan_id,
    subscriptionId
  });
}

Managing Subscriptions

Get Subscription Status

// pages/api/billing/status.js
export default async function handler(req, res) {
  const { shopId } = req.query;
  
  const subscription = await getShopSubscription(shopId);
  
  if (!subscription) {
    return res.json({
      status: 'none',
      plan: null
    });
  }
  
  // Check with Stripe for latest status
  if (subscription.stripeSubscriptionId) {
    const stripeSubscription = await stripe.subscriptions.retrieve(
      subscription.stripeSubscriptionId
    );
    
    return res.json({
      status: stripeSubscription.status,
      plan: subscription.planId,
      currentPeriodEnd: stripeSubscription.current_period_end,
      cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
      trialEnd: stripeSubscription.trial_end
    });
  }
  
  res.json({
    status: subscription.status,
    plan: subscription.planId
  });
}

Upgrade/Downgrade Plan

// pages/api/billing/change-plan.js
export default async function handler(req, res) {
  const { shopId, newPlanId } = req.body;
  
  const subscription = await getShopSubscription(shopId);
  
  if (!subscription?.stripeSubscriptionId) {
    return res.status(400).json({ error: 'No active subscription' });
  }
  
  const newPriceId = getPriceIdForPlan(newPlanId);
  
  // Get current subscription
  const stripeSubscription = await stripe.subscriptions.retrieve(
    subscription.stripeSubscriptionId
  );
  
  // Update subscription
  const updatedSubscription = await stripe.subscriptions.update(
    subscription.stripeSubscriptionId,
    {
      items: [
        {
          id: stripeSubscription.items.data[0].id,
          price: newPriceId
        }
      ],
      proration_behavior: 'create_prorations',  // Prorate the change
      metadata: {
        plan_id: newPlanId
      }
    }
  );
  
  // Update database
  await updateShopSubscription(shopId, {
    planId: newPlanId
  });
  
  res.json({ 
    success: true, 
    subscription: updatedSubscription 
  });
}

Cancel Subscription

// pages/api/billing/cancel.js
export default async function handler(req, res) {
  const { shopId, immediate = false } = req.body;
  
  const subscription = await getShopSubscription(shopId);
  
  if (!subscription?.stripeSubscriptionId) {
    return res.status(400).json({ error: 'No active subscription' });
  }
  
  if (immediate) {
    // Cancel immediately
    await stripe.subscriptions.cancel(subscription.stripeSubscriptionId);
  } else {
    // Cancel at period end
    await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
      cancel_at_period_end: true
    });
  }
  
  res.json({ success: true });
}

Reactivate Canceled Subscription

// pages/api/billing/reactivate.js
export default async function handler(req, res) {
  const { shopId } = req.body;
  
  const subscription = await getShopSubscription(shopId);
  
  if (!subscription?.stripeSubscriptionId) {
    return res.status(400).json({ error: 'No subscription found' });
  }
  
  // Remove cancellation
  await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
    cancel_at_period_end: false
  });
  
  res.json({ success: true });
}

Customer Portal

Let merchants manage their billing through Stripe’s Customer Portal:
// pages/api/billing/portal.js
export default async function handler(req, res) {
  const { shopId, returnUrl } = req.body;
  
  const subscription = await getShopSubscription(shopId);
  
  if (!subscription?.stripeCustomerId) {
    return res.status(400).json({ error: 'No billing account' });
  }
  
  const portalSession = await stripe.billingPortal.sessions.create({
    customer: subscription.stripeCustomerId,
    return_url: returnUrl || 'https://your-app.com/settings/billing'
  });
  
  res.json({ url: portalSession.url });
}

Handling Webhook Events

Subscription Updates

async function handleSubscriptionUpdated(subscription) {
  const shopId = subscription.metadata.shop_id;
  
  await updateShopSubscription(shopId, {
    status: subscription.status,
    currentPeriodEnd: new Date(subscription.current_period_end * 1000),
    cancelAtPeriodEnd: subscription.cancel_at_period_end
  });
  
  // Handle status changes
  switch (subscription.status) {
    case 'active':
      await enableAppFeatures(shopId);
      break;
    case 'past_due':
      await sendPaymentReminderEmail(shopId);
      break;
    case 'canceled':
      await disableAppFeatures(shopId);
      break;
    case 'unpaid':
      await disableAppFeatures(shopId);
      await sendFinalWarningEmail(shopId);
      break;
  }
}

Payment Failures

async function handlePaymentFailed(invoice) {
  const customerId = invoice.customer;
  const shopId = await getShopIdByStripeCustomer(customerId);
  
  // Check attempt count
  const attemptCount = invoice.attempt_count;
  
  if (attemptCount === 1) {
    await sendEmail(shopId, 'payment_failed_first', {
      amount: invoice.amount_due / 100,
      nextAttemptDate: new Date(invoice.next_payment_attempt * 1000)
    });
  } else if (attemptCount >= 3) {
    await sendEmail(shopId, 'payment_failed_final', {
      amount: invoice.amount_due / 100
    });
    
    // Consider downgrading or disabling features
    await updateShopSubscription(shopId, {
      status: 'payment_failed',
      paymentFailedAt: new Date()
    });
  }
}

Testing

Test Mode

Use Stripe test mode during development:
const stripe = new Stripe(
  process.env.NODE_ENV === 'production'
    ? process.env.STRIPE_SECRET_KEY
    : process.env.STRIPE_TEST_SECRET_KEY
);

Test Card Numbers

Card NumberScenario
4242424242424242Successful payment
4000000000000002Declined
4000000000009995Insufficient funds
4000000000000341Attaches but fails charge

Test Webhook Events

Use Stripe CLI to forward webhooks locally:
stripe listen --forward-to localhost:3000/api/webhooks/stripe

Security

Never expose your Stripe secret key in client-side code. All Stripe API calls should be made from your backend.

Webhook Verification

Always verify webhook signatures:
try {
  event = stripe.webhooks.constructEvent(
    rawBody,
    req.headers['stripe-signature'],
    webhookSecret
  );
} catch (err) {
  // Invalid signature - reject the request
  return res.status(400).send('Invalid signature');
}

Idempotency

Use idempotency keys for critical operations:
const subscription = await stripe.subscriptions.create(
  {
    customer: customerId,
    items: [{ price: priceId }]
  },
  {
    idempotencyKey: `sub_create_${shopId}_${Date.now()}`
  }
);

Best Practices

Handle active, past_due, canceled, unpaid, trialing, and incomplete states appropriately.
Email customers before trial ends, when payment fails, and before cancellation takes effect.
Use Stripe’s Smart Retries and send custom reminder emails for failed payments.
Use Stripe’s Customer Portal for payment method updates and invoice history.
Log all webhook events and subscription changes for debugging and auditing.
Test subscription creation, upgrades, downgrades, cancellations, and payment failures.

See Also