PayPal Integration
Complete guide for integrating PayPal payments with BareCommerce. Orders are created automatically when PayPal webhooks arrive.
Why PayPal?
- Global Reach — PayPal available in 200+ countries
- Trust — 429+ million active users
- Multiple Methods — PayPal Wallet, credit cards, local payment methods
- Buyer Protection — Built-in dispute resolution
- Easy Setup — Simple API and webhook configuration
Prerequisites
- PayPal Developer Account (opens in a new tab)
- Node.js backend or serverless function
- BareCommerce store
How It Works
1. Collect order information
2. Create PayPal order with order data in metadata
3. Customer approves on PayPal
4. Capture the payment
5. PayPal sends webhook to BareCommerce
6. ✅ BareCommerce creates order automaticallySetup
1. Install Dependencies
npm install @paypal/checkout-server-sdk2. Environment Variables
# PayPal
PAYPAL_MODE=sandbox # or production
PAYPAL_CLIENT_ID=your_client_id
PAYPAL_CLIENT_SECRET=your_client_secret
PAYPAL_WEBHOOK_ID=your_webhook_id3. PayPal Webhook Setup
- Go to PayPal Developer Dashboard (opens in a new tab)
- Select Sandbox or Production
- Go to Apps & Credentials
- Select your app
- Go to Webhooks
- Click Create Webhook
- Webhook URL:
https://yourdomain.com/api/webhooks/paypal - Select events to receive:
PAYMENT.CAPTURE.COMPLETEDPAYMENT.CAPTURE.DENIEDPAYMENT.CAPTURE.REFUNDED
- Click Create Webhook
- Copy the Webhook ID to
PAYPAL_WEBHOOK_ID
Backend: Create PayPal Order Endpoint
Create an endpoint that generates PayPal orders with order data in the custom_id:
// pages/api/paypal/create-order.ts
import { NextRequest, NextResponse } from 'next/server';
import * as paypal from '@paypal/checkout-server-sdk';
const client = new paypal.core.PayPalHttpClient(
new paypal.core.SandboxEnvironment(
process.env.PAYPAL_CLIENT_ID,
process.env.PAYPAL_CLIENT_SECRET
)
);
export async function POST(request: NextRequest) {
try {
const {
storeId,
customerId,
items,
subtotal,
tax,
shipping,
discount,
total,
currency,
shippingAddress
} = await request.json();
if (!storeId || !customerId || !items?.length || !total) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
// Prepare order metadata for BareCommerce
// This will be sent via webhook
const customIdData = {
store_id: storeId,
customer_id: customerId,
items: items,
subtotal,
tax,
shipping,
discount
};
// Create PayPal order
const createRequest = new paypal.orders.OrdersCreateRequest();
createRequest.prefer('return=representation');
createRequest.requestBody({
intent: 'CAPTURE',
purchase_units: [
{
reference_id: 'BARECOMMERCE_ORDER',
amount: {
currency_code: currency,
value: total.toString(),
breakdown: {
item_total: {
currency_code: currency,
value: subtotal.toString()
},
tax_total: {
currency_code: currency,
value: tax.toString()
},
shipping: {
currency_code: currency,
value: shipping.toString()
},
discount: {
currency_code: currency,
value: discount.toString()
}
}
},
items: items.map((item: any) => ({
name: item.productId,
quantity: item.quantity.toString(),
unit_amount: {
currency_code: currency,
value: item.unitPrice.toString()
}
})),
shipping: {
name: {
full_name: shippingAddress?.name || 'Customer'
},
address: {
address_line_1: shippingAddress?.address1,
address_line_2: shippingAddress?.address2,
admin_area_2: shippingAddress?.city,
admin_area_1: shippingAddress?.region,
postal_code: shippingAddress?.postalCode,
country_code: shippingAddress?.country
}
},
// Store order metadata in custom_id for webhook
custom_id: JSON.stringify(customIdData)
}
]
});
const response = await client.execute(createRequest);
return NextResponse.json({
id: response.result.id,
status: response.result.status
});
} catch (error: any) {
console.error('PayPal order creation failed:', error);
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}Frontend: PayPal Checkout
// components/PayPalCheckout.tsx
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
declare global {
namespace window {
var paypal: any;
}
}
export function PayPalCheckout({
cartItems,
customerInfo,
shippingAddress,
onSuccess
}: {
cartItems: CartItem[];
customerInfo: { customerId: string; storeId: string };
shippingAddress: Address;
onSuccess: (orderId: string) => void;
}) {
const router = useRouter();
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;
const shipping = 10.0;
const discount = 0;
const total = subtotal + tax + shipping - discount;
// Load PayPal script
useEffect(() => {
const script = document.createElement('script');
script.src = `https://www.paypal.com/sdk/js?client-id=${process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID}`;
script.async = true;
script.onload = initPayPal;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
const initPayPal = () => {
if (!window.paypal) return;
window.paypal.Buttons({
createOrder: async () => {
console.log('Creating PayPal order...');
try {
// Step 1: Create PayPal order with order metadata
const response = await fetch('/api/paypal/create-order', {
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 (!response.ok) {
throw new Error('Failed to create PayPal order');
}
const { id } = await response.json();
console.log('✓ PayPal order created:', id);
return id;
} catch (err: any) {
setError(err.message);
console.error('Order creation failed:', err);
throw err;
}
},
onApprove: async (data: any) => {
console.log('Customer approved, capturing payment...');
try {
// Step 2: Capture the payment
const response = await fetch('/api/paypal/capture-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderID: data.orderID
})
});
if (!response.ok) {
throw new Error('Failed to capture payment');
}
console.log('✓ Payment captured');
// 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: Order is ready
onSuccess(data.orderID);
} catch (err: any) {
setError(err.message);
console.error('Capture failed:', err);
}
},
onError: (err: any) => {
setError('Payment failed. Please try again.');
console.error('PayPal error:', err);
},
onCancel: () => {
setError('Payment cancelled.');
console.log('Payment cancelled');
}
}).render('#paypal-button-container');
};
return (
<div>
{error && (
<div className="mb-4 p-4 bg-red-50 text-red-700 rounded">
{error}
</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>
<div id="paypal-button-container" />
</div>
);
}Backend: Capture and Webhook Handler
Capture Endpoint
// pages/api/paypal/capture-order.ts
import { NextRequest, NextResponse } from 'next/server';
import * as paypal from '@paypal/checkout-server-sdk';
const client = new paypal.core.PayPalHttpClient(
new paypal.core.SandboxEnvironment(
process.env.PAYPAL_CLIENT_ID,
process.env.PAYPAL_CLIENT_SECRET
)
);
export async function POST(request: NextRequest) {
try {
const { orderID } = await request.json();
if (!orderID) {
return NextResponse.json(
{ error: 'orderID is required' },
{ status: 400 }
);
}
// Capture the payment
const captureRequest = new paypal.orders.OrdersCaptureRequest(orderID);
captureRequest.requestBody({});
const response = await client.execute(captureRequest);
if (response.result.status === 'COMPLETED') {
console.log('✓ Payment captured:', response.result.id);
return NextResponse.json({
status: 'COMPLETED',
orderID: response.result.id
});
} else {
throw new Error(`Payment not completed. Status: ${response.result.status}`);
}
} catch (error: any) {
console.error('Capture failed:', error);
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}Webhook Handler
// pages/api/webhooks/paypal.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
const WEBHOOK_ID = process.env.PAYPAL_WEBHOOK_ID || '';
const CLIENT_ID = process.env.PAYPAL_CLIENT_ID || '';
const CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET || '';
/**
* Verify PayPal webhook signature via PayPal API
*/
async function verifyPayPalWebhook(
transmissionId: string,
transmissionTime: string,
certUrl: string,
authAlgo: string,
transmissionSig: string,
webhookBody: string
): Promise<boolean> {
try {
// Get access token
const tokenResponse = await fetch('https://api.sandbox.paypal.com/v1/oauth2/token', {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'grant_type=client_credentials'
});
const tokenData = await tokenResponse.json();
const accessToken = tokenData.access_token;
// Verify signature with PayPal
const verifyResponse = await fetch(
'https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature',
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
transmission_id: transmissionId,
transmission_time: transmissionTime,
cert_url: certUrl,
auth_algo: authAlgo,
transmission_sig: transmissionSig,
webhook_id: WEBHOOK_ID,
webhook_event: JSON.parse(webhookBody)
})
}
);
const verifyData = await verifyResponse.json();
return verifyData.verification_status === 'SUCCESS';
} catch (error) {
console.error('PayPal verification error:', error);
return false;
}
}
export async function POST(request: NextRequest) {
try {
const transmissionId = request.headers.get('paypal-transmission-id') || '';
const transmissionTime = request.headers.get('paypal-transmission-time') || '';
const certUrl = request.headers.get('paypal-cert-url') || '';
const authAlgo = request.headers.get('paypal-auth-algo') || '';
const transmissionSig = request.headers.get('paypal-transmission-sig') || '';
const webhookBody = await request.text();
// Verify webhook signature
const isValid = await verifyPayPalWebhook(
transmissionId,
transmissionTime,
certUrl,
authAlgo,
transmissionSig,
webhookBody
);
if (!isValid) {
console.error('Invalid PayPal webhook signature');
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Parse event
const event = JSON.parse(webhookBody);
console.log(`[PayPal] Received event: ${event.event_type} (${event.id})`);
try {
switch (event.event_type) {
case 'PAYMENT.CAPTURE.COMPLETED': {
const capture = event.resource;
console.log(`✓ Payment completed: ${capture.id}`);
// Extract custom_id from purchase units
const customIdStr = capture.supplementary_data?.related_ids?.order_reference_id;
if (!customIdStr) {
console.warn('No custom_id in webhook');
return NextResponse.json({ received: true });
}
const orderData = JSON.parse(customIdStr);
console.log('Order data from webhook:', orderData);
// IMPORTANT: BareCommerce order is created here from webhook
// You would normally call a BareCommerce ingestion function
// For now, just log that we received it
break;
}
case 'PAYMENT.CAPTURE.DENIED': {
const capture = event.resource;
console.log(`✗ Payment denied: ${capture.id}`);
console.log(`Reason: ${capture.status_details?.reason}`);
break;
}
case 'PAYMENT.CAPTURE.REFUNDED': {
const capture = event.resource;
console.log(`↩ Payment refunded: ${capture.id}`);
break;
}
default:
console.log(`Unhandled event type: ${event.event_type}`);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Event processing error:', error);
return NextResponse.json(
{ error: 'Event processing failed' },
{ status: 500 }
);
}
} catch (error: any) {
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 Accounts
Use these test credentials in Sandbox:
- Personal Account (Buyer): Provided by PayPal Dashboard
- Business Account (Seller): Provided by PayPal Dashboard
Test End-to-End
- Set
PAYPAL_MODE=sandboxin.env - Start your app:
npm run dev - Go to checkout page
- Click "Pay with PayPal"
- Log in with PayPal sandbox account
- Approve the payment
- Watch logs for webhook delivery
- Verify order was created
Local Testing with ngrok
# 1. Start ngrok
ngrok http 3000
# 2. Update webhook URL in PayPal Dashboard with ngrok URL
# 3. Run your app
npm run dev
# 4. Test payment flowProduction Checklist
- Switch to live credentials (
PAYPAL_MODE=production) - Update webhook URL to production domain
- Test with small real payment
- Webhook signature verification enabled
- Error handling for failed payments
- Refund handling implemented
- Monitoring/alerting configured
- Payment confirmation emails working
- SSL certificate valid
Related Guides
- Payments Overview — Payment provider overview
- Stripe Integration — Stripe setup
- Square Integration — Square setup
- Webhooks Guide — How webhooks work