How to Process Payments with the Square API 2026
Square handles both online and in-person payments with a single API. This guide covers online payments: embedding the payment form, creating charges, managing customers, and handling subscriptions.
TL;DR
Square's main advantage over Stripe is unified online and in-person payments under one dashboard — critical if you have a physical retail component. For pure online or SaaS businesses, Stripe has better documentation and a larger SDK ecosystem. Square charges no monthly fee, and the online transaction rate of 2.9% + $0.30 matches Stripe exactly. If you already run Square's point-of-sale hardware, extending into online payments requires almost no additional setup. If you're starting from scratch with a web-only product, weigh the documentation quality gap carefully before committing.
What You'll Build
- Embedded payment form (credit card)
- Server-side payment processing
- Customer management
- Invoice creation
- Recurring subscriptions
Prerequisites: Node.js 18+, Square account (free, no monthly fee).
Square's Position in the Developer Payments Market
Square built its name in physical retail — the little card reader that plugged into a phone headphone jack made it ubiquitous in farmers markets, food trucks, and independent coffee shops. Over the past several years the company has been aggressively expanding its developer tooling to compete with Stripe in the online space. The result is a platform that genuinely excels at the hybrid use case: businesses that sell both in-person and online. A restaurant taking table-side card payments and online orders through the same system, or a fitness studio managing walk-in clients and monthly memberships, gets real value from Square's unified reporting and single API surface. Where Square still trails Stripe is in the breadth of its SDK ecosystem and the depth of its documentation — Stripe's reference docs and interactive examples remain the industry benchmark. For developers building pure SaaS products with no physical presence, that gap matters. But for the retail-adjacent use cases Square was built for, the platform is mature, reliable, and increasingly capable.
1. Setup
Install
npm install square
Initialize
// lib/square.ts
import { Client, Environment } from 'square';
export const squareClient = new Client({
accessToken: process.env.SQUARE_ACCESS_TOKEN!,
environment: Environment.Sandbox, // Change to Production for live
});
export const { paymentsApi, customersApi, invoicesApi } = squareClient;
Environment Variables
SQUARE_ACCESS_TOKEN=EAAAl...
SQUARE_APPLICATION_ID=sandbox-sq0idb-...
SQUARE_LOCATION_ID=L...
NEXT_PUBLIC_SQUARE_APPLICATION_ID=sandbox-sq0idb-...
NEXT_PUBLIC_SQUARE_LOCATION_ID=L...
2. Payment Form
Square's Web Payments SDK uses a tokenization model that keeps raw card data off your servers entirely. When a customer enters their card number, expiration date, and CVV into the Square-hosted input elements, the SDK communicates directly with Square's servers to exchange that sensitive data for a short-lived, single-use token. Your application only ever touches this token — never the actual card details. This approach is how Square handles PCI compliance on your behalf: by keeping the sensitive data out of your request pipeline, your application qualifies for the lightest PCI assessment tier (SAQ A), which requires minimal documentation and no security scans. The tokenize step happens entirely in the browser before any data hits your API route.
Load Web Payments SDK
// components/SquarePayment.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
export function SquarePayment({ amount }: { amount: number }) {
const cardRef = useRef<HTMLDivElement>(null);
const [card, setCard] = useState<any>(null);
const [processing, setProcessing] = useState(false);
useEffect(() => {
const loadSquare = async () => {
const payments = (window as any).Square.payments(
process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID!,
process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID!
);
const card = await payments.card();
await card.attach(cardRef.current!);
setCard(card);
};
// Load Square SDK script
const script = document.createElement('script');
script.src = 'https://sandbox.web.squarecdn.com/v1/square.js';
script.onload = loadSquare;
document.head.appendChild(script);
}, []);
const handlePayment = async () => {
if (!card) return;
setProcessing(true);
try {
const result = await card.tokenize();
if (result.status !== 'OK') {
throw new Error('Card tokenization failed');
}
// Send token to your server
const res = await fetch('/api/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sourceId: result.token,
amount,
}),
});
const data = await res.json();
if (data.success) {
alert('Payment successful!');
}
} catch (error) {
console.error('Payment failed:', error);
} finally {
setProcessing(false);
}
};
return (
<div>
<div ref={cardRef} style={{ minHeight: '100px' }} />
<button onClick={handlePayment} disabled={processing || !card}>
{processing ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`}
</button>
</div>
);
}
3. Process Payment (Server)
// app/api/payments/route.ts
import { NextResponse } from 'next/server';
import { paymentsApi } from '@/lib/square';
import { randomUUID } from 'crypto';
export async function POST(req: Request) {
const { sourceId, amount } = await req.json();
try {
const response = await paymentsApi.createPayment({
sourceId,
idempotencyKey: randomUUID(),
amountMoney: {
amount: BigInt(amount), // Amount in cents
currency: 'USD',
},
locationId: process.env.SQUARE_LOCATION_ID!,
});
return NextResponse.json({
success: true,
paymentId: response.result.payment?.id,
status: response.result.payment?.status,
});
} catch (error: any) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 400 }
);
}
}
4. Customer Management
Persisting customer records in Square pays dividends well beyond the initial transaction. Once a customer exists in your Square account, you can attach saved cards for one-click future purchases, associate them with subscriptions, include them as invoice recipients, and pull per-customer revenue reports directly from the Square dashboard. Square's customer model also integrates with their marketing tools — email campaigns, loyalty programs, and appointment scheduling all reference the same customer entity. For subscription-based products especially, the investment in proper customer management at signup time eliminates a lot of pain when it comes to billing, cancellation, and churn analysis down the road.
// Create customer
const customer = await customersApi.createCustomer({
givenName: 'Jane',
familyName: 'Doe',
emailAddress: 'jane@example.com',
phoneNumber: '+15551234567',
idempotencyKey: randomUUID(),
});
// Save card on file for future payments
const card = await squareClient.cardsApi.createCard({
sourceId: tokenFromWebPaymentsSDK,
idempotencyKey: randomUUID(),
card: {
customerId: customer.result.customer!.id!,
},
});
// Charge saved card
await paymentsApi.createPayment({
sourceId: card.result.card!.id!,
idempotencyKey: randomUUID(),
amountMoney: { amount: BigInt(2900), currency: 'USD' },
customerId: customer.result.customer!.id!,
locationId: process.env.SQUARE_LOCATION_ID!,
});
5. Invoices
Square Invoices is a strong fit for service-based businesses that bill on completion of work rather than at checkout. Freelancers, contractors, consultants, and B2B service providers all benefit from the workflow: create an invoice, attach line items, email it to the client, and let Square handle payment collection and reminders. The invoice rate of 3.3% + $0.30 is slightly higher than a standard payment, but the operational overhead it removes — chasing unpaid invoices, reconciling partial payments, manually sending reminders — often justifies the cost for professional services contexts. Square's invoice dashboard also provides a clean paper trail that simplifies bookkeeping for sole proprietors and small teams.
// Create an invoice
const invoice = await invoicesApi.createInvoice({
invoice: {
locationId: process.env.SQUARE_LOCATION_ID!,
primaryRecipient: {
customerId: customerId,
},
paymentRequests: [{
requestType: 'BALANCE',
dueDate: '2026-04-01',
automaticPaymentSource: 'NONE',
}],
deliveryMethod: 'EMAIL',
title: 'Consulting Services',
},
idempotencyKey: randomUUID(),
});
// Add line items via order
const order = await squareClient.ordersApi.createOrder({
order: {
locationId: process.env.SQUARE_LOCATION_ID!,
lineItems: [{
name: 'Web Development',
quantity: '10',
basePriceMoney: { amount: BigInt(15000), currency: 'USD' }, // $150/hr
}],
},
idempotencyKey: randomUUID(),
});
// Publish (send) the invoice
await invoicesApi.publishInvoice(invoiceId, {
version: 0,
idempotencyKey: randomUUID(),
});
6. Subscriptions
Square's subscription model builds on top of its Catalog API rather than existing as a standalone billing engine the way Stripe Billing does. You define a subscription plan as a catalog object — specifying billing cadence and price — then subscribe customers by linking them to a plan plus a saved payment method. The approach is more rigid than Stripe's, which allows per-seat pricing, usage-based metering, and multi-currency plans out of the box. Square subscriptions work well for simple recurring models like monthly memberships or annual plans at a fixed price. For complex billing logic — trial periods that convert automatically, tiered pricing, invoice-level tax handling across jurisdictions — Stripe Billing has a more complete feature set and significantly more documentation. If your subscription needs are straightforward and you already use Square for in-person payments, keeping everything in one platform simplifies reconciliation considerably.
// Create a subscription plan (catalog item)
const plan = await squareClient.catalogApi.upsertCatalogObject({
idempotencyKey: randomUUID(),
object: {
type: 'SUBSCRIPTION_PLAN',
id: '#pro-plan',
subscriptionPlanData: {
name: 'Pro Plan',
phases: [{
cadence: 'MONTHLY',
recurringPriceMoney: { amount: BigInt(2900), currency: 'USD' },
}],
},
},
});
// Subscribe a customer
const subscription = await squareClient.subscriptionsApi.createSubscription({
locationId: process.env.SQUARE_LOCATION_ID!,
planId: plan.result.catalogObject!.id!,
customerId: customerId,
cardId: savedCardId,
idempotencyKey: randomUUID(),
});
Pricing
| Feature | Cost |
|---|---|
| Online payments | 2.9% + $0.30 per transaction |
| In-person payments | 2.6% + $0.10 per tap/dip |
| Invoices | 3.3% + $0.30 per invoice payment |
| Recurring billing | 3.5% + $0.15 per payment |
| Monthly fee | $0 (no subscription required) |
Square vs Stripe
| Feature | Square | Stripe |
|---|---|---|
| Online rate | 2.9% + $0.30 | 2.9% + $0.30 |
| In-person | 2.6% + $0.10 | 2.7% + $0.05 |
| POS hardware | ✅ First-party | ❌ Third-party only |
| Dashboard | ✅ Unified online + in-person | ✅ Online focused |
| Free tier | ✅ No monthly fee | ✅ No monthly fee |
| Developer docs | Good | Excellent |
The comparison table above captures the mechanical differences, but the decision usually comes down to business model rather than features or price. Choose Square when your business has a physical retail or service component — a shop, a studio, a restaurant, a market stall — and you want a single system for both in-person and online sales. The unified dashboard, first-party hardware, and integrated inventory and reporting make Square genuinely better for that use case. Choose Stripe when you're building an API-first product: a SaaS platform, a marketplace, a developer tool, or anything where flexibility in billing logic and the breadth of the integration ecosystem matter more than hardware compatibility. Stripe's documentation, webhook tooling, and third-party integrations are ahead of Square's for software-centric businesses. For a deeper head-to-head, see our Square vs Stripe API comparison. If you're evaluating a broader set of options, the best payment APIs for 2026 roundup covers pricing and developer experience across the major providers. For teams that ultimately choose Stripe, the step-by-step guide to adding Stripe payments in Next.js covers the full implementation in detail.
Idempotency and Error Handling
Every mutating API call in Square — creating a payment, creating a customer, publishing an invoice — accepts an idempotencyKey parameter. This is not optional ceremony; it is the primary mechanism for safe retry logic. When a network timeout or server error occurs mid-request, you often cannot tell whether the operation completed on Square's end before the failure. Resubmitting the same request without an idempotency key risks creating a duplicate charge. Resubmitting with the same idempotencyKey value is safe — Square will recognize the key, find the original completed operation, and return its result without processing a second charge.
The standard pattern is to generate a UUID per operation attempt and store it alongside the pending order in your database. If the payment request fails and the user hits retry, your server reuses the same UUID for that order rather than generating a fresh one. Square deduplicates against idempotency keys for 24 hours. This pattern is equally important for subscription creation and invoice publishing — both operations have real monetary consequences if accidentally duplicated.
For production deployments, set up Square webhooks to receive payment status updates rather than relying solely on synchronous API responses. Webhook events like payment.completed, payment.failed, and subscription.updated allow your system to stay in sync with Square's state even when a user closes the browser tab mid-checkout or a network issue interrupts the response. Square's webhook payloads include a signature header you should validate against your webhook signature key to confirm the event originated from Square rather than a malicious third party.
The BigInt type used for monetary amounts is a frequent source of confusion for developers new to the Square SDK. Square represents all monetary values in the smallest currency unit — cents for USD — and the TypeScript SDK types these as BigInt to avoid floating-point precision issues. JavaScript's native number type cannot safely represent integers above 2^53, which while far larger than any realistic transaction amount, creates type errors at the SDK boundary if you pass a regular number. Always use BigInt(amount) when constructing amountMoney objects.
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Not using idempotency keys | Duplicate charges on retry | Always include idempotencyKey |
| Using Sandbox keys in production | Payments don't process | Switch to Production environment |
| Processing BigInt amounts wrong | Wrong charge amount | Amounts are in cents as BigInt |
| Not handling card tokenization errors | Silent failures | Check result.status before sending token |
| Missing location ID | API calls fail | Every payment needs a locationId |
Choosing a payment API? See our Square vs Stripe API comparison and the best payment APIs for 2026 — pricing, features, and developer experience.