Stripe Integration
Complete guide for integrating Stripe payments with BareCommerce. Orders are created automatically when Stripe webhooks arrive, so you don't need to make any BareCommerce API calls.
Why Stripe?
- Developer Experience — Best-in-class API and documentation
- Modern Checkout — Embedded payments that stay on your site
- Built-in Fraud Detection — Stripe Radar included
- Subscriptions — First-class recurring billing support
- 40+ Countries — Available in major markets
Prerequisites
- Stripe account (create one (opens in a new tab))
- BareCommerce store (for webhook receiver)
- Node.js backend or serverless function
How It Works
1. Your frontend collects cart info
2. Your backend creates Stripe PaymentIntent with order metadata
3. Frontend collects payment info
4. Stripe processes payment
5. Stripe sends webhook to BareCommerce
6. ✅ BareCommerce creates order automaticallyImportant: You do NOT create orders in BareCommerce. Orders are created from webhook data.
Setup
1. Install Dependencies
npm install stripe2. Environment Variables
# .env.local or .env.production
STRIPE_SECRET_KEY=sk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
# For Stripe webhooks to reach BareCommerce
# (This is configured on Stripe Dashboard, not as env var)3. Stripe Webhook Setup
- Go to Stripe Dashboard (opens in a new tab) → Developers → Webhooks
- Click Add an endpoint
- Endpoint URL:
https://yourdomain.com/api/webhooks/stripe - Select events to send:
payment_intent.succeeded(order is paid)payment_intent.payment_failed(payment failed)charge.refunded(refund processed)
- Copy the Signing secret to
STRIPE_WEBHOOK_SECRET
Backend: Create PaymentIntent Endpoint
Create an endpoint that generates payment intents with all order data:
// pages/api/payment-intents.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2023-10-16'
});
export async function POST(request: NextRequest) {
try {
const {
storeId,
customerId,
items,
subtotal,
tax,
shipping,
discount,
total,
currency,
shippingAddress
} = await request.json();
// Validate required fields
if (!storeId || !customerId || !items?.length || !total) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
// Create PaymentIntent with all order metadata
// This data will be sent to BareCommerce via webhook
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(parseFloat(total) * 100), // Stripe uses cents
currency: currency.toLowerCase(),
// CRITICAL: Include all order data in metadata
// BareCommerce will receive this via webhook
metadata: {
store_id: storeId,
customer_id: customerId,
items: JSON.stringify(items),
subtotal: subtotal.toString(),
tax: tax.toString(),
shipping: shipping.toString(),
discount: discount.toString(),
shippingAddress: JSON.stringify(shippingAddress),
},
automatic_payment_methods: { enabled: true }
});
return NextResponse.json({
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id
});
} catch (error: any) {
console.error('PaymentIntent creation failed:', error);
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}Frontend: Collect Payment
Step 1: Collect Order Information
// pages/checkout.tsx
import { useState } from 'react';
import { loadStripe } from '@stripe/js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
);
export function CheckoutForm({
cartItems,
customerInfo,
shippingAddress,
onSuccess
}: {
cartItems: CartItem[];
customerInfo: { customerId: string; storeId: string };
shippingAddress: Address;
onSuccess: (orderId: string) => void;
}) {
const stripe = useStripe();
const elements = useElements();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Calculate totals
const subtotal = cartItems.reduce((sum, item) => sum + (parseFloat(item.price) * item.quantity), 0);
const tax = subtotal * 0.08; // 8% tax
const shipping = 10.00;
const discount = 0;
const total = subtotal + tax + shipping - discount;
const handlePayment = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setLoading(true);
setError(null);
try {
// Step 1: Create PaymentIntent on your backend
console.log('Creating payment intent...');
const piResponse = await fetch('/api/payment-intents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
storeId: customerInfo.storeId,
customerId: customerInfo.customerId,
items: cartItems.map(item => ({
productId: item.productId,
quantity: item.quantity,
unitPrice: item.price
})),
subtotal: subtotal.toFixed(2),
tax: tax.toFixed(2),
shipping: shipping.toFixed(2),
discount: discount.toFixed(2),
total: total.toFixed(2),
currency: 'USD',
shippingAddress
})
});
if (!piResponse.ok) {
throw new Error('Failed to create payment intent');
}
const { clientSecret, paymentIntentId } = await piResponse.json();
console.log('✓ Payment intent created:', paymentIntentId);
// Step 2: Confirm payment with Stripe
console.log('Confirming payment...');
const result = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: elements.getElement(CardElement)!,
billing_details: {
name: 'Customer Name',
email: customerInfo.email,
address: {
line1: shippingAddress.address1,
city: shippingAddress.city,
state: shippingAddress.region,
postal_code: shippingAddress.postalCode,
country: shippingAddress.country
}
}
}
});
if (result.error) {
throw new Error(result.error.message);
}
if (result.paymentIntent?.status === 'succeeded') {
console.log('✓ Payment succeeded:', result.paymentIntent.id);
// Step 3: Wait for webhook to create order
console.log('Waiting for order to be created via webhook...');
await new Promise(resolve => setTimeout(resolve, 2000));
// Step 4: Redirect to confirmation
// Note: Order is already created in BareCommerce from webhook
onSuccess(paymentIntentId);
}
} catch (err: any) {
setError(err.message);
console.error('Payment failed:', err);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handlePayment}>
{error && (
<div className="mb-4 p-4 bg-red-50 text-red-700 rounded">
{error}
</div>
)}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Card Details</label>
<div className="p-4 border border-gray-300 rounded">
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': { color: '#aab7c4' }
},
invalid: { color: '#fa755a' }
}
}}
/>
</div>
</div>
<div className="mb-4 p-4 bg-gray-50 rounded">
<div className="flex justify-between mb-2">
<span>Subtotal:</span>
<span>${subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between mb-2">
<span>Tax:</span>
<span>${tax.toFixed(2)}</span>
</div>
<div className="flex justify-between mb-4">
<span>Shipping:</span>
<span>${shipping.toFixed(2)}</span>
</div>
<div className="flex justify-between font-bold text-lg border-t pt-2">
<span>Total:</span>
<span>${total.toFixed(2)}</span>
</div>
</div>
<button
type="submit"
disabled={!stripe || loading}
className="w-full bg-blue-600 text-white py-3 rounded font-medium disabled:opacity-50"
>
{loading ? 'Processing...' : `Pay $${total.toFixed(2)}`}
</button>
</form>
);
}
// Wrap with Stripe Elements provider
export default function CheckoutPage() {
const [orderCreated, setOrderCreated] = useState(false);
if (orderCreated) {
return (
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">✓ Order Created!</h2>
<p>Your order has been automatically created and is ready for fulfillment.</p>
</div>
);
}
return (
<Elements stripe={stripePromise}>
<CheckoutForm
cartItems={/* your cart */}
customerInfo={{ customerId: '...', storeId: '...' }}
shippingAddress={/* address */}
onSuccess={() => setOrderCreated(true)}
/>
</Elements>
);
}Backend: Webhook Handler
BareCommerce calls your webhook endpoint when payment succeeds. Important: You should NOT handle the BareCommerce webhook yourself—instead, configure Stripe to send webhooks directly to BareCommerce.
However, if you want to do additional processing (send confirmation email, update your database), you can listen for Stripe events on your own endpoint:
// pages/api/webhooks/stripe-events.ts
// (Optional - only if you need to do additional processing)
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import crypto from 'crypto';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '');
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || '';
async function verifyStripeSignature(
payload: string,
signature: string,
secret: string
): Promise<boolean> {
try {
// Parse signature header format: "t=timestamp,v1=signature"
const parts = signature.split(',');
const signatureMap: Record<string, string> = {};
for (const part of parts) {
const [key, value] = part.split('=');
signatureMap[key] = value;
}
const timestamp = signatureMap['t'];
const expectedSignature = signatureMap['v1'];
if (!timestamp || !expectedSignature) return false;
// Verify timestamp is recent (5 minute tolerance)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) {
console.warn('Webhook timestamp too old:', age);
return false;
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(signedPayload);
const computed = hmac.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(computed)
);
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
export async function POST(request: NextRequest) {
const payload = await request.text();
const signature = request.headers.get('stripe-signature') || '';
// Verify webhook is from Stripe
const isValid = await verifyStripeSignature(payload, signature, webhookSecret);
if (!isValid) {
console.error('Invalid Stripe signature');
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// Parse event
let event: Stripe.Event;
try {
event = JSON.parse(payload);
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}
console.log(`[Stripe] Received event: ${event.type} (${event.id})`);
try {
switch (event.type) {
case 'payment_intent.succeeded': {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
console.log(`✓ Payment succeeded: ${paymentIntent.id}`);
// IMPORTANT: Don't create order here—BareCommerce already created it via its webhook
// This handler is just for your own additional processing
// Example: Send confirmation email
// await sendConfirmationEmail(paymentIntent.metadata?.customer_id);
// Example: Update your database
// await updatePaymentStatus(paymentIntent.id, 'completed');
break;
}
case 'payment_intent.payment_failed': {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
console.log(`✗ Payment failed: ${paymentIntent.id}`);
console.log(`Error: ${paymentIntent.last_payment_error?.message}`);
// Example: Notify customer
// await sendFailureEmail(paymentIntent.metadata?.customer_id);
break;
}
case 'charge.refunded': {
const charge = event.data.object as Stripe.Charge;
console.log(`↩ Refund processed: ${charge.id}`);
console.log(`Amount: $${(charge.amount_refunded || 0) / 100}`);
// Example: Update refund status
// await updateRefundStatus(charge.payment_intent, 'completed');
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error(`Webhook handler error:`, error);
return NextResponse.json(
{ error: 'Webhook handler failed' },
{ status: 500 }
);
}
}
export async function GET() {
return NextResponse.json(
{ error: 'Method not allowed' },
{ status: 405 }
);
}Testing
Test Cards
| Card | Result | Use Case |
|---|---|---|
4242 4242 4242 4242 | Succeeds | Happy path |
4000 0000 0000 0002 | Declines | Payment decline |
4000 0025 0000 3155 | Requires auth | 3D Secure |
5555 5555 5555 4444 | Succeeds | Mastercard |
Use any future expiry date (e.g., 12/25) and any 3-digit CVC.
Local Testing with Stripe CLI
# 1. Install Stripe CLI
brew install stripe/stripe-cli/stripe
# 2. Authenticate
stripe login
# 3. Forward webhook requests to your local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# 4. Start your app
npm run dev
# 5. Trigger a test event
stripe trigger payment_intent.succeededTest End-to-End
- Start Stripe CLI:
stripe listen --forward-to localhost:3000/... - Run app:
npm run dev - Go to checkout page
- Enter test card:
4242 4242 4242 4242 - Submit payment
- Check logs for webhook delivery
- Verify order was created in BareCommerce
Production Checklist
- Switch to live Stripe keys
- Webhook URL is production domain
- Webhook signature verification enabled
- Test with small real payment
- Error handling implemented for failed payments
- Refund handling implemented
- Monitoring/alerting configured
- Payment confirmation emails working
- Fraud detection enabled on Stripe Dashboard
- SSL certificate valid
Troubleshooting
Order Not Creating?
- Check webhook delivery: Stripe Dashboard → Developers → Webhooks → Click endpoint → View events
- Verify metadata: Ensure
store_id,customer_id,itemsare in metadata - Check logs: Look for webhook processing errors
- Test signature verification: Ensure webhook secret is correct
Payment Intent Not Creating?
- Check API key: Ensure
STRIPE_SECRET_KEYis set and correct - Verify required fields:
amount,currency,metadatamust be provided - Check response: Log the full error from Stripe API
Webhook Signature Invalid?
- Copy exact secret: Ensure you copied
whsec_...from Stripe Dashboard - Don't modify payload: Use raw request body for verification
- Check timestamp: Stripe rejects webhooks older than 5 minutes
Related Guides
- Payments Overview — Payment provider overview
- Webhooks Guide — How webhooks work
- PayPal Integration — PayPal setup
- Square Integration — Square setup