Payments
Square

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

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 automatically

Setup

1. Install Dependencies

npm install square

2. Environment Variables

# Square
SQUARE_ACCESS_TOKEN=your_access_token
SQUARE_LOCATION_ID=your_location_id
SQUARE_WEBHOOK_SIGNATURE_KEY=your_signature_key

3. Square Webhook Setup

  1. Go to Square Developer Dashboard (opens in a new tab)
  2. Select your application
  3. Go to Webhooks
  4. Click Add an Endpoint
  5. Endpoint URL: https://yourdomain.com/api/webhooks/square
  6. Select events:
    • payment.created
    • payment.updated
    • refund.created
  7. 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 NumberResultCVCExpiry
4111111111111111SuccessAnyFuture
5555555555554444SuccessAnyFuture
378282246310005SuccessAnyFuture
6011111111111117SuccessAnyFuture

Local Testing

  1. Set credentials in .env.local
  2. Use test mode in Square Dashboard
  3. Start your app: npm run dev
  4. Go to checkout
  5. Enter test card
  6. Submit payment
  7. 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