Usage & Integration
Practical examples of using Stripe payments in Plainform with checkout, webhooks, and pricing display
Learn how to use Stripe payments in your Plainform application with practical examples.
Creating Checkout Sessions
Create a checkout session to accept payments. Plainform uses a form-based approach:
export function PricingCard({ priceId, couponId }) {
return (
<form action="/api/stripe/checkout" method="POST">
<input type="hidden" name="priceId" value={priceId} />
{couponId && (
<input type="hidden" name="couponId" value={couponId} />
)}
<button type="submit">Buy Now</button>
</form>
);
}Checkout API Route
The checkout route creates a Stripe session with manual capture:
import { stripe } from '@/lib/stripe/stripe';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const formData = await req.formData();
const priceId = formData.get('priceId')?.toString();
const couponId = formData.get('couponId')?.toString();
if (!priceId) {
return NextResponse.json(
{ message: 'Invalid request' },
{ status: 400 }
);
}
try {
// Get price to determine payment mode
const price = await stripe.prices.retrieve(priceId);
const mode = price.type === 'recurring' ? 'subscription' : 'payment';
const session = await stripe.checkout.sessions.create({
line_items: [{ price: priceId, quantity: 1 }],
discounts: couponId ? [{ coupon: couponId }] : undefined,
mode,
success_url: `${process.env.SITE_URL}/order?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.SITE_URL}`,
automatic_tax: { enabled: true },
// Manual capture for one-time payments
...(mode === 'payment' && {
payment_intent_data: {
capture_method: 'manual',
},
}),
});
return NextResponse.redirect(session.url!, { status: 303 });
} catch (error) {
console.error('Checkout error:', error);
return NextResponse.json(
{ message: 'Checkout failed' },
{ status: 500 }
);
}
}Key points:
- Determines mode (payment vs subscription) from price type
- Uses manual capture for one-time payments
- Redirects to Stripe-hosted checkout page
- Includes automatic tax calculation
Handling Webhooks
Webhooks notify your app of payment events. The webhook route verifies signatures and processes events:
import { stripe } from '@/lib/stripe/stripe';
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get('stripe-signature')!;
let event;
try {
// Verify webhook signature
event = stripe.webhooks.constructEvent(
rawBody,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (error) {
console.error('Webhook signature verification failed:', error);
return NextResponse.json(
{ message: 'Invalid signature' },
{ status: 400 }
);
}
// Process events
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(event.data.object);
revalidateTag('stripe/orders');
break;
case 'product.created':
case 'product.updated':
case 'product.deleted':
revalidateTag('stripe/products');
break;
case 'coupon.created':
case 'coupon.updated':
revalidateTag('stripe/coupons');
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
}Key points:
- Always verify webhook signature for security
- Use
req.text()to get raw body (required for signature verification) - Revalidate Next.js cache tags after data changes
- Handle events asynchronously
Processing Checkout Completion
When a checkout completes, capture the payment and send confirmation:
async function handleCheckoutCompleted(session) {
const paymentIntentId = session.payment_intent;
const customerEmail = session.customer_details?.email;
if (!customerEmail) {
// Cancel payment if email missing
await cancelPaymentIntent(paymentIntentId);
return;
}
try {
// Capture payment (for manual capture mode)
if (paymentIntentId) {
await capturePaymentIntent(paymentIntentId);
}
// Update session metadata
await stripe.checkout.sessions.update(session.id, {
metadata: {
orderStatus: 'Payment successful',
paymentStatus: 'Accepted',
},
});
// Send confirmation email
await sendOrderConfirmation(customerEmail);
return { success: true };
} catch (error) {
console.error('Payment capture failed:', error);
await cancelPaymentIntent(paymentIntentId);
throw error;
}
}Key points:
- Validates customer email before processing
- Captures payment for manual capture mode
- Updates session metadata for order tracking
- Sends confirmation email
- Cancels payment on error
Manual Payment Capture
Plainform uses manual capture to give you control over when payments are charged. This allows you to implement custom business logic between payment authorization and capture.
Why manual capture?
- Run custom validation logic before charging
- Verify inventory availability
- Grant user access or credits
- Provision resources (API keys, licenses, etc.)
- Send custom notifications
- Update your database before finalizing payment
Capture or cancel payments using helper functions:
Capture Payment
import { stripe } from '@/lib/stripe/stripe';
export async function capturePaymentIntent(paymentIntentId: string) {
try {
const paymentIntent = await stripe.paymentIntents.capture(
paymentIntentId
);
return {
ok: true,
paymentIntent,
orderStatus: 'Payment captured successfully',
};
} catch (error) {
console.error('Capture failed:', error);
return {
ok: false,
orderStatus: 'Payment capture failed',
message: 'Unable to process payment',
};
}
}Cancel Payment
import { stripe } from '@/lib/stripe/stripe';
export async function cancelPaymentIntent(paymentIntentId: string) {
try {
const paymentIntent = await stripe.paymentIntents.cancel(
paymentIntentId
);
return {
ok: true,
paymentIntent,
message: 'Payment cancelled successfully',
};
} catch (error) {
console.error('Cancel failed:', error);
return {
ok: false,
message: 'Unable to cancel payment',
};
}
}Use cases:
- Capture: After running your custom business logic (inventory check, user provisioning, etc.)
- Cancel: If validation fails, order is invalid, or customer requests cancellation
Example workflow:
// 1. Payment authorized (not charged yet)
// 2. Run your custom logic
const inventoryAvailable = await checkInventory(productId);
const userAccount = await getUserAccount(customerEmail);
// 3. Capture or cancel based on your logic
if (inventoryAvailable && userAccount) {
await capturePaymentIntent(paymentIntentId);
await grantUserAccess(userAccount.id, productId);
await sendWelcomeEmail(customerEmail);
} else {
await cancelPaymentIntent(paymentIntentId);
await notifyCustomer('Order could not be processed');
}You can also capture or cancel payments manually in the Stripe Dashboard under Payments → Uncaptured.
Displaying Products & Pricing
Fetch products from Stripe and display them in your pricing section:
Fetch Products
export async function getProducts() {
try {
const res = await fetch(`${process.env.SITE_URL}/api/stripe/products`, {
cache: 'force-cache',
next: { tags: ['stripe/products'] },
});
if (!res.ok) {
throw new Error('Failed to fetch products');
}
return res.json();
} catch (error) {
console.error('Product fetch error:', error);
return null;
}
}Products API Route
import { stripe } from '@/lib/stripe/stripe';
import { NextResponse } from 'next/server';
export async function GET() {
try {
const products = await stripe.products.list({
active: true,
expand: ['data.default_price'],
});
const activeProducts = products.data.filter((p) => p.active);
const sortedProducts = activeProducts.sort((a, b) => a.created - b.created);
return NextResponse.json({ products: sortedProducts, ok: true });
} catch (error) {
console.error('Products fetch error:', error);
return NextResponse.json(
{ message: 'Failed to fetch products' },
{ status: 500 }
);
}
}Display Pricing
import { getProducts } from '@/lib/stripe/getProducts';
import { getCoupons } from '@/lib/stripe/getCoupons';
import { PricingCard } from './PricingCard';
export async function Pricing() {
const products = await getProducts();
const coupons = await getCoupons();
if (!products) {
return <div>Unable to load pricing</div>;
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{products.products.map((product) => (
<PricingCard
key={product.id}
name={product.name}
description={product.description}
price={product.default_price.unit_amount / 100}
currency={product.default_price.currency}
priceId={product.default_price.id}
priceType={product.default_price.type}
features={product.marketing_features}
couponId={coupons?.coupon?.id}
discount={coupons?.coupon?.percent_off}
/>
))}
</div>
);
}Key points:
- Products fetched server-side with caching
- Prices divided by 100 (Stripe uses cents)
- Coupons applied if available
- Cached with revalidation tags
Fetch Coupons
Display active coupons in your pricing section:
export async function getCoupons() {
try {
const res = await fetch(`${process.env.SITE_URL}/api/stripe/coupons`, {
cache: 'force-cache',
next: { tags: ['stripe/coupons'] },
});
if (!res.ok) {
throw new Error('Failed to fetch coupons');
}
return res.json();
} catch (error) {
console.error('Coupon fetch error:', error);
return null;
}
}Cache Revalidation
Revalidate cached data when Stripe data changes:
switch (event.type) {
case 'product.created':
case 'product.updated':
case 'product.deleted':
revalidateTag('stripe/products');
break;
case 'coupon.created':
case 'coupon.updated':
case 'coupon.deleted':
revalidateTag('stripe/coupons');
break;
case 'checkout.session.completed':
revalidateTag('stripe/orders');
break;
}Why revalidation matters:
- Products and coupons are cached for performance
- Webhooks trigger cache updates when data changes
- Ensures pricing section always shows current data
Testing Payments
Test the payment flow with Stripe test cards:
- Create Checkout: Click "Buy Now" on pricing page
- Use Test Card:
4242 4242 4242 4242 - Complete Checkout: Use any future date and CVC
- Check Webhook: Verify webhook received in Stripe Dashboard
- Verify Order: Check order saved to database (if applicable)
- Capture Payment: Capture in Stripe Dashboard → Payments
In test mode, use Stripe test cards to simulate different scenarios like declined cards, authentication required, etc.
Next Steps
How is this guide ?
Last updated on