Implementing Payments with Next.js × Square: Introduction Guide to the Square Web Payments SDK
Introduction
This article introduces how to implement payment processing using Square. Square is a service that provides online and offline payment methods, with rich APIs for developers that allow flexible customization. A key advantage is that you can process payments securely without having to store credit card information on the application side.
Using the example of building a payment system with Next.js, this article explains in detail how to introduce Square, database design, processing flow, and concrete implementation. It should be useful for those considering introducing payment processing with Square or looking for a reference implementation.
Implementation image
When the purchase button is clicked, the user is redirected to Square’s payment screen.
After entering the required information and executing the “Pay” process, the payment is completed.
All input of credit card numbers and similar information is done on a screen provided by Square, and the application does not store card numbers or other such information.
What is Square? (For web developers)
Square is a payment service that allows you to easily introduce online and offline payment processing. It provides POS systems, e‑commerce payments, subscriptions, and APIs, enabling developers to customize flexibly.
Features from a developer’s perspective
1️. Supports multiple payment methods
- Credit cards (Visa, Mastercard, Amex, JCB, etc.)
- Debit cards
- Mobile payments (Apple Pay, Google Pay)
- Electronic invoices and subscriptions
- Rich RESTful APIs
- Payments API: Handles credit card and mobile payments
- Orders API: Enables order management
- Invoices API: Issues and manages electronic invoices
- Subscriptions API: Sets up recurring billing
- Customers API: Centralized management of customer data
👉 Benefit: Simple REST APIs make it easy to handle from both backend and frontend
- Works in serverless and cloud environments
- Can be used with AWS Lambda and Firebase Functions
- Real‑time processing of completed payments via Webhooks
👉 Benefit: Reduces backend management costs
Benefit: Reduces backend management costs
- Compliant with PCI DSS
- Square manages storage and processing of card information
- Built‑in security measures (tokenization, encryption)
👉 Benefit: Reduces the effort required for PCI DSS compliance
Square’s official Next.js integration blog
Square actually provides an example implementation with Next.js (App Router) in an official blog post.
At first I thought using this would be the fastest way to implement, but I found a few minor issues and gave up on it.
In this example, the Next.js client screen is configured to prompt for credit card input.
The input itself uses a library that includes components and validation.
Using components like this is convenient, but:
- You want to remember the credit card and reuse it from the second time onward
- You want to support payment methods other than credit cards
Because such requirements were on the horizon, I decided not to use it.
If you just want to quickly implement credit card payments only, the above approach is fine, but it did not match the requirements this time, so I abandoned it.
System architecture
The frontend screens are provided by Next.js, with MySQL as the data store and Square for payment processing.
Basically, the Next.js server side integrates with these backend services.
For payment processing, the credit card input screen is provided by Square, so the arrow extends from Square to the user.
ER diagram
This is the ER diagram of the DB.
In the actual service, many tables are used, but for this article only the following three tables are needed:
- Course: Something like the content that users purchase
- Purchase: Users’ purchase history
- SquareCustomer: A table that links the userId managed by the service with the customer ID managed by Square
SquareCustomer does not have relations with any other table.
Its only role is to convert userId when accessing Square, obtaining necessary information, and linking it with the service to provide some information.
Processing flow
Before going into the detailed implementation, here is the processing flow from the start to the end of a payment.
-
Click the purchase button
-
The Next.js server side (using an API in this case) receives the request
It receives the request content and performs validation as needed. -
The server queries the DB to see whether the user in the request has a Square customer ID
If the customer ID does not exist, it requests Square to issue a new customer ID and registers it in the DB (SquareCustomer table) -
From the Next.js server side, send a request to Square to issue a URL for the payment screen
-
Return the URL of the payment screen received from Square to the user
-
The user enters the credit card number and other information on the payment screen and executes the payment
-
Square processes the payment
-
If the payment completes successfully, Square sends a payment completion notification via Webhook
-
The Next.js server side registers the purchase history in the DB
Uses the Purchase table -
The user views the purchased content
Implementation
Based on the above processing flow, here is some supplementary explanation of what the code looks like in each implementation.
- Click the purchase button
This is the implementation of the purchase screen.
getChapter accesses the DB to check whether the relevant course has already been purchased.
If it has not been purchased, the CourseEnrollButton button is displayed.
const ChapterIdPage = async ({
params
}: {
params: { courseId: string; chapterId: string }
}) => {
const { userId } = auth();
if (!userId) {
return redirect("/");
}
const {
chapter,
course,
muxData,
attachments,
nextChapter,
userProgress,
purchase,
} = await getChapter({
userId,
chapterId: params.chapterId,
courseId: params.courseId,
});
return (
<div>
<div className="flex flex-col max-w-4xl mx-auto pb-20">
<div>
<div className="p-4 flex flex-col md:flex-row items-center justify-between">
<h2 className="text-2xl font-semibold mb-2">
{chapter.title}
</h2>
{purchase ? (
<CourseProgressButton
chapterId={params.chapterId}
courseId={params.courseId}
nextChapterId={nextChapter?.id}
isCompleted={!!userProgress?.isCompleted}
/>
) : (
<CourseEnrollButton
courseId={params.courseId}
price={course.price!}
/>
)}
</div>
Next is the implementation of the purchase button.
When the purchase button is clicked, it accesses the API (/api/courses/${courseId}/checkout), obtains the URL from the response, and opens it in a separate page.
How the URL is generated and returned to the client side is explained in “4. Request from the Next.js server side to Square to issue a URL for the payment screen”.
export const CourseEnrollButton = ({
price,
courseId,
}: CourseEnrollButtonProps) => {
const [isLoading, setIsLoading] = useState(false);
const onClick = async () => {
try {
setIsLoading(true);
const response = await axios.post(`/api/courses/${courseId}/checkout`)
window.location.assign(response.data.url);
} catch {
toast.error("An error occurred");
} finally {
setIsLoading(false);
}
}
return (
<Button
onClick={onClick}
disabled={isLoading}
size="sm"
className="min-w-[140px]"
>
{isLoading ? <div className="w-full flex items-center justify-center"><Loader2 className="h-6 w-6 animate-spin text-secondary" /></div> : `Sale Price ${formatPrice(price)}`}
</Button>
)
}
- The Next.js server side (using an API in this case) receives the request
This is the concrete implementation of the API (/api/courses/${courseId}/checkout).
In this project we use an authentication service called Clerk, and by using currentUser we can obtain user information on the server side.
Using that, we check whether the user exists and validate the request, such as whether the course (content) being purchased this time exists.
export async function POST(
req: Request,
{ params }: { params: { courseId: string } }
) {
try {
const user = await currentUser();
if (!user || !user.id || !user.emailAddresses?.[0]?.emailAddress) {
return new NextResponse("Unauthorized", { status: 401 });
}
const course = await db.course.findUnique({
where: {
id: params.courseId,
isPublished: true,
},
include: {
chapters: true
}
});
const purchase = await db.purchase.findUnique({
where: {
userId_courseId: {
userId: user.id,
courseId: params.courseId
}
}
});
if (purchase) {
return new NextResponse("Already purchased", { status: 400 });
}
if (!course) {
return new NextResponse("Not found", { status: 404 });
}
if (!course.price) {
return new NextResponse("Not for sale", { status: 400 });
}
if (course.chapters.length === 0) {
return new NextResponse("No chapters", { status: 400 });
}
- Query the DB to see whether the user in the request has a Square customer ID
Using db.squareCustomer.findUnique, we check whether a Square customer ID has been registered.
If a customer ID has not yet been created, we use the email of the user who accessed the endpoint to create a Square customer ID via client.customersApi.createCustomer.
We then register it in the DB.
export async function POST(
req: Request,
{ params }: { params: { courseId: string } }
) {
try {
const user = await currentUser();
if (!user || !user.id || !user.emailAddresses?.[0]?.emailAddress) {
return new NextResponse("Unauthorized", { status: 401 });
}
const client = new Client({
environment: Environment.Production,
accessToken: process.env.SQUARE_ACCESS_TOKEN!
});
let squareCustomer = await db.squareCustomer.findUnique({
where: {
userId: user.id,
},
select: {
squareCustomerId: true,
}
});
if (!squareCustomer) {
const response = await client.customersApi.createCustomer({
emailAddress: user.emailAddresses[0].emailAddress,
});
squareCustomer = await db.squareCustomer.create({
data: {
userId: user.id,
squareCustomerId: response.result.customer?.id || '',
}
});
}
- Request from the Next.js server side to Square to issue a URL for the payment screen
We send a request to issue a URL for the payment screen using client.checkoutApi.createPaymentLink.
To return to the content screen after the user completes the payment on the payment screen, we specify the redirect URL using checkoutOptions.
Overview of createPaymentLink()
const response = await client.checkoutApi.createPaymentLink({...});
- Uses client.checkoutApi.createPaymentLink() to create a Square payment link.
- When the buyer clicks the link, they are redirected to the payment screen, and when the payment is completed, Square sends a Webhook.
- response contains information about the created payment link (such as the URL).
Parameters of the payment request
idempotencyKey: Ensuring idempotency of the request
idempotencyKey: uuidv4(),
- Uses uuidv4() to generate a unique ID (UUID).
- A mechanism to prevent duplicate payments even if the same request is sent multiple times.
order: Order information
order: {
locationId: process.env.SQUARE_LOCATION_ID!,
customerId: squareCustomer.squareCustomerId,
lineItems: [...],
metadata: {...}
}
- locationId (location ID): Specifies the location (store) where payment processing is performed in Square.
- customerId: Customer ID registered in Square.
- lineItems: Details of the product being purchased (name, quantity, price, etc.).
- metadata: Embeds additional information (such as course_id) that can be referenced later.
lineItems: Purchase item information
lineItems: [
{
name: course.title,
quantity: '1',
itemType: 'ITEM',
metadata: {
'course_id': course.id,
'user_id': user.id,
},
basePriceMoney: {
amount: BigInt(course.price),
currency: 'JPY'
}
}
]
Details of each purchase item:
- name: Product name (e.g., course.title).
- quantity: Quantity of the product being purchased (1 in this case).
- itemType: Type of product ("ITEM").
- metadata: Additional information per item (course ID, user ID).
- basePriceMoney: Price information of the product.
- amount: Amount (an integer value using BigInt(course.price)).
- currency: Currency (JPY = Japanese yen).
checkoutOptions: Redirect URL after payment
checkoutOptions: {
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/courses/${course.id}/chapters/${course.chapters[0].id}`
}
- Specifies the URL to redirect to after the payment is completed.
- Configured so that after the buyer purchases the content, they are redirected to the first chapter page (course.chapters[0]).
prePopulatedData: Pre‑filled data
prePopulatedData: {
buyerEmail: user.emailAddresses[0].emailAddress,
}
- Pre‑fills the buyer’s email address.
- On Square’s payment screen, the user’s email address field is automatically filled.
paymentNote: Payment note
paymentNote: course.id
- A memo to associate the course ID with the payment transaction.
- Makes it easy to identify which course the payment is for in the Square dashboard.
Here is the full code:
const response = await client.checkoutApi.createPaymentLink({
idempotencyKey: uuidv4(),
order: {
locationId: process.env.SQUARE_LOCATION_ID!,
customerId: squareCustomer.squareCustomerId,
lineItems: [
{
name: course.title,
quantity: '1',
itemType: 'ITEM',
metadata: {
'course_id': course.id,
'user_id': user.id,
},
basePriceMoney: {
amount: BigInt(course.price),
currency: 'JPY'
}
}
],
metadata: {
'course_id': course.id,
}
},
checkoutOptions: {
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/courses/${course.id}/chapters/${course.chapters[0].id}`
},
prePopulatedData: {
buyerEmail: user.emailAddresses[0].emailAddress,
},
paymentNote: course.id
});
- Return the URL of the payment screen received from Square to the user
We set the URL received from Square as the response returned to the client side.
In this article the code is split up, but in reality the code for steps 2–5 is a single sequence of processing in the API.
return NextResponse.json({ url: response.result.paymentLink?.longUrl });
- The user enters the credit card number and other information on the payment screen and executes the payment
When the user opens the screen at the URL received in step 5, they can perform the payment process.
This is internal processing within Square.
-
Square processes the payment
-
If the payment completes successfully, Square sends a payment completion notification via Webhook
Before implementing the process that receives the payment completion notification, you need to configure the URL that will receive the payment completion notification in Square.
On the webhook settings screen, configure the destination URL for notifications.
You also need to configure which events to notify; in this case, payment.craeted and payment.updated are configured.
Next is the configuration of the Next.js API that receives the notifications.
This API provides an endpoint for processing Square Webhook events. It uses the POST method to receive Webhook requests sent from Square. Below are notes on each part of the processing.
Obtaining the request body
- Uses req.text() to obtain the request body (JSON data) as a string.
- Since Webhook requests are sent as application/json, handling it as a string prevents modification during signature verification.
export async function POST(req: Request) {
const body = await req.text();
Verifying Square’s signature
const signature = headers().get("x-square-hmacsha256-signature") || "";
- Square’s Webhook request includes a header called x-square-hmacsha256-signature.
- This is an HMAC signature that Square adds to guarantee the legitimacy of the request.
- If the signature cannot be obtained, an empty string "" is assigned (though this will cause an error during verification).
Verifying the Webhook signature
const result = WebhooksHelper.isValidWebhookEventSignature(
body,
signature,
SIGNATURE_KEY,
NOTIFICATION_URL
);
- Uses WebhooksHelper.isValidWebhookEventSignature() to verify the validity of the signature.
- SIGNATURE_KEY is the Square Webhook secret key (configured in the dashboard).
- NOTIFICATION_URL is the URL of this endpoint (the Webhook destination configured on the Square side).
- If the signature is invalid, the request is considered invalid.
Parsing the Webhook event
const event = JSON.parse(body);
- Parses the received request body (JSON string) into a JavaScript object.
- The Webhook payload includes type (event type) and data (detailed data).
Decomposing the received data
const { type, data } = event;
const { object } = data;
const { payment } = object;
const { note, customer_id, order_id, status } = payment;
- Expands the data based on the structure of Square Webhook events.
- type contains the event type (e.g., "payment.created").
- The payment object contains payment information (order ID, customer ID, note, payment status, etc.).
Conditions for processing the Webhook
if (result && type === 'payment.created' && status === 'APPROVED') {
- result is true (signature verification OK), and
- type is "payment.created" (event where a payment was newly created), and
- status is "APPROVED" (approved payment)
only then does processing continue. - This allows you to filter out unnecessary Webhook requests.
Checking required data
if (!customer_id || !note || !order_id) {
throw new Error('Webhook Error: Missing metadata');
}
- If any of customer_id (Square customer ID), note (memo information), or order_id (order ID) is missing, an error is thrown.
- This validation prevents processing from continuing with incomplete data.
Searching for the Square customer ID
const user = await db.squareCustomer.findUnique({
where: {
squareCustomerId: customer_id,
}
});
- Searches for user information in the database based on the Square customer_id.
- db.squareCustomer.findUnique() is the process that uses the DB to obtain unique user information.
Error handling when the customer does not exist
if (!user) {
throw new Error('Webhook Error: User not found');
}
- If the corresponding user is not found, an error is thrown.
- This prevents processing information for non‑existent customers.
Here is the full code:
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get("x-square-hmacsha256-signature") || "";
try {
const result = WebhooksHelper.isValidWebhookEventSignature(
body,
signature,
SIGNATURE_KEY,
NOTIFICATION_URL
);
const event = JSON.parse(body);
console.log('event', JSON.stringify(event, null, 2));
const { type, data } = event
const { object } = data
const { payment } = object
const { note, customer_id, order_id, status } = payment
if (result && type === 'payment.created' && status === 'APPROVED' ) {
if (!customer_id || !note || !order_id) {
throw new Error('Webhook Error: Missing metadata');
}
const user = await db.squareCustomer.findUnique({
where: {
squareCustomerId: customer_id,
}
});
if (!user) {
throw new Error('Webhook Error: User not found');
}
- Registering purchase history in the DB on the Next.js server side
Saving purchase information
await db.purchase.create({
data: {
courseId: String(note),
userId: user.userId,
orderId: order_id,
}
});
- If the payment is approved, the purchase information (course ID, user ID, order ID) is saved in the database.
- note is used as metadata related to the payment and is set as courseId.
Success response
return new NextResponse(`Webhook Success: ${type}`, { status: 200 });
- If processing of the Webhook event succeeds, a 200 OK response is returned along with the message “Webhook Success: type”.
- The user views the purchased content
Using the post‑payment redirect URL configured in checkoutOptions in “4. Request from the Next.js server side to Square to issue a URL for the payment screen”, the user is redirected to the content viewing screen and can start watching.
Conclusion
By using Square, we saw that it is possible to easily build a secure and flexible payment system. In particular, the fact that you do not need to store credit card information on the application side greatly reduces the burden of security measures, which is a major advantage.
Also, by leveraging the integration between the frontend and backend using Next.js, we were able to implement the payment processing flow smoothly. By combining Square’s diverse APIs, it is also possible to introduce even more advanced payment features in the future.
I hope this article will be of some help to developers considering introducing a payment system using Square.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Chat App (with Image/PDF Sending and Video Call Features)
2024/07/15Practical Component Design Guide with React × Tailwind CSS × Emotion: The Optimal Approach to Design Systems, State Management, and Reusability
2024/11/22How to Easily Build a Web API with Express and MongoDB [TypeScript Compatible]
2024/12/09Express (+ TypeScript) Beginner’s Guide: How to Quickly Build Web Applications
2024/12/07Complete Guide to Refactoring React: Improve Your Code with Modularization, Render Optimization, and Design Patterns
2025/01/13Management Dashboard Features (Graph Display, Data Import)
2024/06/02Test Automation with Jest and TypeScript: A Complete Guide from Basic Setup to Writing Type-Safe Tests
2023/09/13ESLint / Prettier Introduction Guide: Thorough Explanation from Husky, CI/CD Integration, to Visualizing Code Quality
2024/02/12




