Square Integration
Complete guide for integrating Square payments with BareCommerce. Orders are created automatically when Square webhooks arrive.
Why Square?
- Omnichannel — Unified online + in-person payment processing
- Developer-Friendly — Well-documented API and SDKs
- Inventory Sync — Manage inventory across channels
- Point of Sale — Accept payments in-store and online
- All Payment Methods — Cards, digital wallets, local payments
Prerequisites
- Square Developer Account (opens in a new tab)
- Node.js backend or serverless function
- BareCommerce store
How It Works
1. Collect order information
2. Create payment with order data in reference_id
3. Customer authorizes payment
4. Square processes payment
5. Square sends webhook to BareCommerce
6. ✅ BareCommerce creates order automaticallySetup
1. Install Dependencies
npm install square2. Environment Variables
# Square
SQUARE_ACCESS_TOKEN=your_access_token
SQUARE_LOCATION_ID=your_location_id
SQUARE_WEBHOOK_SIGNATURE_KEY=your_signature_key3. Square Webhook Setup
- Go to Square Developer Dashboard (opens in a new tab)
- Select your application
- Go to Webhooks
- Click Add an Endpoint
- Endpoint URL:
https://yourdomain.com/api/webhooks/square - Select events:
payment.createdpayment.updatedrefund.created
- Copy the Signature Key to
SQUARE_WEBHOOK_SIGNATURE_KEY
Backend: Create Payment Endpoint
// pages/api/square/create-payment.ts
import { NextRequest, NextResponse } from 'next/server';
import { Client, Environment } from 'square';
const client = new Client({
accessToken: process.env.SQUARE_ACCESS_TOKEN,
environment: Environment.Production
});
const paymentsApi = client.getPaymentsApi();
export async function POST(request: NextRequest) {
try {
const {
storeId,
customerId,
items,
subtotal,
tax,
shipping,
discount,
total,
currency,
shippingAddress,
sourceId, // nonce from web payment form
verificationToken // from digital wallet
} = await request.json();
if (!storeId || !customerId || !items?.length || !total) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
if (!sourceId && !verificationToken) {
return NextResponse.json(
{ error: 'Payment source (sourceId or verificationToken) is required' },
{ status: 400 }
);
}
// Prepare order metadata for BareCommerce
const referenceId = JSON.stringify({
store_id: storeId,
customer_id: customerId,
items,
subtotal,
tax,
shipping,
discount,
shippingAddress
});
// Create payment
const body = {
sourceId: sourceId || undefined,
verificationToken: verificationToken || undefined,
amountMoney: {
amount: Math.round(parseFloat(total) * 100), // Square uses cents
currency: currency
},
autocomplete: true,
orderId: crypto.randomUUID(), // Square Order ID
referenceId: referenceId, // Our order metadata
receiptNumberSetting: {
receiptNumber: `BC-${Date.now()}`
},
receiptUrl: 'https://yoursite.com/receipt',
note: `BareCommerce Order - Items: ${items.length}`,
customerId: customerId,
customerNote: `Thank you for your order`,
statementDescriptionIdentifier: 'BARECOMMERCE'
};
const response = await paymentsApi.createPayment(body);
if (!response.result || !response.result.payment?.id) {
throw new Error('Payment creation failed');
}
console.log('✓ Payment created:', response.result.payment.id);
return NextResponse.json({
paymentId: response.result.payment.id,
status: response.result.payment.status,
receiptNumber: response.result.payment.receiptNumber
});
} catch (error: any) {
console.error('Payment creation failed:', error);
return NextResponse.json(
{ error: error.message || 'Payment creation failed' },
{ status: 500 }
);
}
}Frontend: Square Payment Form
// components/SquarePaymentForm.tsx
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
declare global {
namespace window {
var Square: any;
}
}
export function SquarePaymentForm({
cartItems,
customerInfo,
shippingAddress,
onSuccess
}: {
cartItems: CartItem[];
customerInfo: { customerId: string; storeId: string };
shippingAddress: Address;
onSuccess: (paymentId: string) => void;
}) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [web, setWeb] = useState<any>(null);
const [payments, setPayments] = useState<any>(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;
// Initialize Square
useEffect(() => {
const initSquare = async () => {
const squareWeb = window.Square.web;
const initializeResponse = await squareWeb.payments(
process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID,
process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID
);
if (initializeResponse.errors?.length > 0) {
console.error('Square payment errors:', initializeResponse.errors);
return;
}
setWeb(squareWeb);
setPayments(initializeResponse);
// Initialize payment methods
const cardContainer = document.getElementById('sq-cardNumber');
if (cardContainer && initializeResponse.cardNumber) {
await initializeResponse.cardNumber.attach(cardContainer);
}
};
const script = document.createElement('script');
script.src = 'https://web.squarecdn.com/v1/square.js';
script.async = true;
script.onload = initSquare;
document.head.appendChild(script);
}, []);
const handlePayment = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
if (!payments || !payments.cardNumber) {
throw new Error('Payment form not initialized');
}
// Step 1: Request payment token
console.log('Requesting payment token...');
const tokenResult = await payments.cardNumber.requestCardNonce();
if (tokenResult.status === 'NETWORK_ERROR') {
throw new Error('Network error. Please try again.');
}
if (tokenResult.status !== 'OK' || !tokenResult.nonce) {
throw new Error('Failed to get payment token');
}
const sourceId = tokenResult.nonce;
console.log('✓ Payment token received');
// Step 2: Create payment with BareCommerce order metadata
console.log('Creating payment...');
const paymentResponse = await fetch('/api/square/create-payment', {
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,
sourceId
})
});
if (!paymentResponse.ok) {
throw new Error('Payment creation failed');
}
const { paymentId } = await paymentResponse.json();
console.log('✓ Payment created:', paymentId);
// 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(paymentId);
} 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 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 className="mb-4">
<label className="block text-sm font-medium mb-2">Card Number</label>
<div id="sq-cardNumber" className="p-4 border border-gray-300 rounded" />
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium mb-2">Expiration</label>
<div id="sq-expiration" className="p-4 border border-gray-300 rounded" />
</div>
<div>
<label className="block text-sm font-medium mb-2">CVC</label>
<div id="sq-cvv" className="p-4 border border-gray-300 rounded" />
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Postal Code</label>
<div id="sq-postalCode" className="p-4 border border-gray-300 rounded" />
</div>
<button
type="submit"
disabled={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>
);
}Backend: Webhook Handler
// pages/api/webhooks/square.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
const webhookSignatureKey = process.env.SQUARE_WEBHOOK_SIGNATURE_KEY || '';
/**
* Verify Square webhook signature
*/
function verifySquareSignature(
payload: string,
signature: string,
signatureKey: string,
requestUrl: string
): boolean {
try {
// Get the request URL without query params
const notificationUrl = requestUrl.split('?')[0];
// Construct the message to verify
// Format: {notification_url}{payload}
const message = notificationUrl + payload;
// Compute HMAC SHA256
const hmac = crypto.createHmac('sha256', signatureKey);
hmac.update(message);
const computed = hmac.digest('base64');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(signature)
);
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
export async function POST(request: NextRequest) {
try {
const payload = await request.text();
const signature = request.headers.get('x-square-hmac-sha256-signature') || '';
// Verify webhook signature
const isValid = verifySquareSignature(
payload,
signature,
webhookSignatureKey,
request.url
);
if (!isValid) {
console.error('Invalid Square webhook signature');
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Parse event
const event = JSON.parse(payload);
console.log(`[Square] Received event: ${event.type} (${event.data.id})`);
try {
switch (event.type) {
case 'payment.created':
case 'payment.updated': {
const payment = event.data.object?.payment;
if (!payment) {
console.warn('No payment object in event');
return NextResponse.json({ received: true });
}
console.log(`✓ Payment processed: ${payment.id}`);
console.log(`Status: ${payment.status}`);
console.log(`Amount: ${payment.amount_money?.amount / 100} ${payment.amount_money?.currency}`);
// Extract order data from reference_id
if (payment.reference_id) {
try {
const orderData = JSON.parse(payment.reference_id);
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
} catch {
console.warn('Could not parse reference_id as JSON');
}
}
break;
}
case 'refund.created': {
const refund = event.data.object?.refund;
if (!refund) {
console.warn('No refund object in event');
return NextResponse.json({ received: true });
}
console.log(`↩ Refund processed: ${refund.id}`);
console.log(`Amount: ${refund.amount_money?.amount / 100} ${refund.amount_money?.currency}`);
break;
}
default:
console.log(`Unhandled event type: ${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 Cards
| Card Number | Result | CVC | Expiry |
|---|---|---|---|
4111111111111111 | Success | Any | Future |
5555555555554444 | Success | Any | Future |
378282246310005 | Success | Any | Future |
6011111111111117 | Success | Any | Future |
Local Testing
- Set credentials in
.env.local - Use test mode in Square Dashboard
- Start your app:
npm run dev - Go to checkout
- Enter test card
- Submit payment
- Check logs for webhook
Production Checklist
- Switch to live access token
- Update webhook URL to production domain
- Webhook signature verification enabled
- Test with small real payment
- Error handling for declined cards
- Refund handling implemented
- Monitoring/alerting configured
- Payment confirmation emails working
- SSL certificate valid
- PCI compliance verified
Related Guides
- Payments Overview — Payment provider overview
- Stripe Integration — Stripe setup
- PayPal Integration — PayPal setup
- Webhooks Guide — How webhooks work