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
- A Stripe account
- Your app registered in the LaunchMyStore developer portal
- Stripe API keys (test and live)
Setup
1. Connect Stripe Account
In the developer portal, connect your Stripe account:
- Go to Apps > Your App > Billing
- Click Connect Stripe Account
- Complete the Stripe Connect onboarding
- Copy your Stripe webhook signing secret
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 Number | Scenario |
|---|
4242424242424242 | Successful payment |
4000000000000002 | Declined |
4000000000009995 | Insufficient funds |
4000000000000341 | Attaches 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 all subscription states
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