Learn how to easily add in-app purchases to your web app with our step-by-step guide for seamless monetization.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Introduction to In-App Purchases for Web Applications
Implementing in-app purchases in your web application can transform your revenue model and user experience. Unlike mobile apps with standardized stores, web apps require custom implementation approaches. This guide walks you through the process of adding robust, secure in-app purchases to your web application.
Choose the Right Payment Processing Approach
The most common approach for web apps is using Stripe or similar processors that handle the payment infrastructure while giving you control over the experience. Let's focus on implementing this pattern.
Step 1: Design Your Purchase Flow
Before coding, map your purchase flow. Consider:
Step 2: Set Up Your Payment Processor (Using Stripe as Example)
First, create a Stripe account and retrieve your API keys:
// Backend initialization (Node.js example)
const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');
Step 3: Define Your Products and Pricing
Configure your products in your payment processor dashboard or via API:
// Creating a product and price programmatically
const product = await stripe.products.create({
name: 'Pro Plan',
description: 'Access to all premium features',
});
const price = await stripe.prices.create({
product: product.id,
unit_amount: 1999, // $19.99
currency: 'usd',
recurring: {
interval: 'month',
},
});
Step 4: Implement Frontend Payment UI
Add the payment interface in your application:
<!-- Simple payment button -->
<button id="checkout-button" class="payment-button">
Upgrade to Pro Plan
</button>
<script>
document.getElementById('checkout-button').addEventListener('click', function() {
// Call your backend to create a checkout session
fetch('/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
priceId: 'price_1234567890',
})
})
.then(function(response) {
return response.json();
})
.then(function(session) {
// Redirect to Stripe Checkout
return stripe.redirectToCheckout({ sessionId: session.id });
})
.catch(function(error) {
console.error('Error:', error);
});
});
</script>
Step 5: Create Backend Endpoints
Set up the server endpoints to handle payment processing:
// Express.js example
app.post('/create-checkout-session', async (req, res) => {
const { priceId } = req.body;
// Get the authenticated user
const userId = req.user.id;
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode: 'subscription',
success_url: `${YOUR_DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${YOUR_DOMAIN}/canceled`,
client_reference_id: userId, // To identify the customer in webhooks
});
res.json({ id: session.id });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Step 6: Implement Webhook Handling
Set up webhooks to receive real-time payment events:
// Webhook endpoint to handle subscription events
app.post('/webhook', async (req, res) => {
const signature = req.headers['stripe-signature'];
let event;
try {
// Verify the event came from Stripe
event = stripe.webhooks.constructEvent(
req.rawBody, // You need to configure your framework to expose the raw body
signature,
'whsec_your_webhook_signing_secret'
);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed: ${err.message}`);
return res.sendStatus(400);
}
// Handle specific events
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
// Retrieve customer ID from client_reference_id
const userId = session.client_reference_id;
// Activate subscription for user
await activateSubscription(userId, session.subscription);
break;
}
case 'invoice.paid': {
// Continue subscription
const invoice = event.data.object;
await extendSubscription(invoice.subscription);
break;
}
case 'invoice.payment_failed': {
// Handle failed payment
const invoice = event.data.object;
await handleFailedPayment(invoice.subscription);
break;
}
// Add more event handlers as needed
}
res.sendStatus(200);
});
// Functions to update your database
async function activateSubscription(userId, subscriptionId) {
// Update user record in your database
await db.users.update({
where: { id: userId },
data: {
subscriptionId: subscriptionId,
subscriptionStatus: 'active',
planType: 'pro',
// Add additional fields as needed
}
});
}
Step 7: Store Purchase State in Your Database
Create database tables to track subscription status:
-- Example schema for subscription tracking
CREATE TABLE user_subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
subscription_id VARCHAR(255) NOT NULL,
plan_type VARCHAR(50) NOT NULL,
status VARCHAR(50) NOT NULL,
current_period_start TIMESTAMP NOT NULL,
current_period_end TIMESTAMP NOT NULL,
cancel_at_period_end BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Step 8: Implement Feature Access Control
Create middleware or services to check subscription status before allowing access to premium features:
// Example middleware for checking subscription
function requireSubscription(planLevel = 'basic') {
return async (req, res, next) => {
const user = req.user;
// If user isn't logged in
if (!user) {
return res.status(401).json({ error: 'Authentication required' });
}
// Fetch latest subscription data
const subscription = await getSubscriptionStatus(user.id);
if (!subscription || subscription.status !== 'active') {
return res.status(403).json({ error: 'Active subscription required' });
}
// Check plan level if specified
if (planLevel !== 'basic' && subscription.planType !== planLevel) {
return res.status(403).json({
error: `${planLevel} subscription required`
});
}
// Add subscription to request object for later use
req.subscription = subscription;
next();
};
}
// Usage in routes
app.get('/premium-content', requireSubscription('pro'), (req, res) => {
// Handle premium content request
res.json({ premiumContent: 'This is exclusive content' });
});
Step 9: Add Subscription Management UI
Create interfaces for users to manage their subscriptions:
// Generate a customer portal session
app.post('/create-customer-portal-session', async (req, res) => {
const userId = req.user.id;
const userRecord = await db.users.findUnique({
where: { id: userId }
});
// Create a portal session
const session = await stripe.billingPortal.sessions.create({
customer: userRecord.stripeCustomerId,
return_url: `${YOUR_DOMAIN}/account`,
});
// Return the URL to the portal
res.json({ url: session.url });
});
Step 10: Implement Analytics and Reporting
Track purchase metrics to optimize your business:
// Example function to gather subscription metrics
async function getSubscriptionMetrics() {
const now = new Date();
const thirtyDaysAgo = new Date(now.setDate(now.getDate() - 30));
// Active subscriptions
const activeSubscriptions = await db.userSubscriptions.count({
where: { status: 'active' }
});
// New subscriptions in last 30 days
const newSubscriptions = await db.userSubscriptions.count({
where: {
created_at: { gte: thirtyDaysAgo },
status: 'active'
}
});
// Churn rate calculation
const cancelledSubscriptions = await db.userSubscriptions.count({
where: {
updated_at: { gte: thirtyDaysAgo },
status: 'cancelled'
}
});
const churnRate = activeSubscriptions > 0
? (cancelledSubscriptions / activeSubscriptions) * 100
: 0;
return {
activeSubscriptions,
newSubscriptions,
cancelledSubscriptions,
churnRate: `${churnRate.toFixed(2)}%`
};
}
Handling Subscription Lifecycle Events
Create comprehensive handlers for all subscription states:
// Handle various subscription states
async function handleSubscriptionUpdated(subscription) {
const status = subscription.status;
const userId = await getUserIdFromSubscription(subscription.id);
switch (status) {
case 'active':
await updateUserPlan(userId, 'active', subscription.items.data[0].price.product);
await sendEmail(userId, 'subscription_activated');
break;
case 'past_due':
await updateUserPlan(userId, 'past_due');
await sendEmail(userId, 'payment_failed');
break;
case 'canceled':
await updateUserPlan(userId, 'canceled');
await scheduleFeatureDeactivation(userId, subscription.current_period_end);
await sendEmail(userId, 'subscription_canceled');
break;
// Handle other statuses
}
}
Implementing Promotional Offers
Add support for discounts and trial periods:
// Create a checkout session with a coupon
app.post('/create-checkout-session-with-coupon', async (req, res) => {
const { priceId, couponId } = req.body;
const userId = req.user.id;
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
discounts: [
{
coupon: couponId,
},
],
mode: 'subscription',
success_url: `${YOUR_DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${YOUR_DOMAIN}/canceled`,
client_reference_id: userId,
});
res.json({ id: session.id });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Implementing One-Time Purchases
For products that aren't subscription-based:
// Create a one-time purchase checkout
app.post('/create-one-time-checkout', async (req, res) => {
const { priceId } = req.body;
const userId = req.user.id;
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode: 'payment', // One-time payment
success_url: `${YOUR_DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${YOUR_DOMAIN}/canceled`,
client_reference_id: userId,
});
res.json({ id: session.id });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Test Cards and Workflows
Use these Stripe test cards to verify different scenarios:
// Test card numbers to use in development
const testCards = {
success: '4242 4242 4242 4242', // Always succeeds
requiresAuth: '4000 0025 0000 3155', // Requires authentication
declinedCard: '4000 0000 0000 0002', // Always fails
insufficientFunds: '4000 0000 0000 9995' // Insufficient funds failure
};
// Test a subscription renewal failure
async function testFailedRenewal(subscriptionId) {
// This creates a failed invoice
const invoice = await stripe.invoices.create({
customer: customerId,
subscription: subscriptionId,
});
await stripe.invoices.finalizeInvoice(invoice.id);
// This will trigger your webhook with invoice.payment_failed
await stripe.invoices.pay(invoice.id, {
paid_out_of_band: false
}).catch(err => console.log('Expected error:', err.message));
}
Security Considerations
Handling Edge Cases
// Example: Handle subscription pause request
app.post('/pause-subscription', requireAuth, async (req, res) => {
const { subscriptionId, resumeDate } = req.body;
const userId = req.user.id;
// Verify user owns this subscription
const subscription = await db.userSubscriptions.findFirst({
where: {
user_id: userId,
subscription_id: subscriptionId
}
});
if (!subscription) {
return res.status(404).json({ error: 'Subscription not found' });
}
try {
// Pause the subscription
await stripe.subscriptions.update(subscriptionId, {
pause_collection: {
behavior: 'void', // or 'keep_as_draft' to still generate invoices
resumes_at: Math.floor(new Date(resumeDate).getTime() / 1000),
},
});
// Update local database
await db.userSubscriptions.update({
where: { id: subscription.id },
data: {
status: 'paused',
resume_at: resumeDate
}
});
res.json({ message: 'Subscription paused successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Implementing in-app purchases for your web application requires careful planning and attention to detail, but can transform your business model. The implementation patterns above provide a solid foundation that you can adapt to your specific needs.
Remember that the most successful in-app purchase systems are those that blend seamlessly with your user experience. Keep the purchase flow simple, provide clear value propositions, and make subscription management intuitive.
By following these patterns and being mindful of the edge cases, you'll create a robust system that can scale with your business while providing a smooth experience for your users.
Explore the top 3 in-app purchase use cases to boost revenue and enhance user experience in your web app.
Enables access to exclusive or advanced functionality within an application after purchase. Content-as-a-product model creates sustainable revenue streams while maintaining a free entry point for new users.
Provides recurring access to continually updated content, features, or services. Creates predictable revenue streams and incentivizes ongoing product development to maintain subscriber retention.
Offers limited-use items that enhance the user experience and require repurchase after consumption. Creates renewable purchase opportunities while maintaining app accessibility for non-paying users.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.