はじめに
本記事では、Squareを活用した決済処理の実装について紹介します。Squareは、オンライン・オフラインの決済手段を提供するサービスで、開発者向けのAPIも充実しており、柔軟なカスタマイズが可能です。特に、クレジットカード情報をアプリケーション側で保持せずに安全に決済を行える点が魅力です。
今回は、Next.jsを用いた決済システムの構築を例に、Squareの導入方法、データベース設計、処理フロー、具体的な実装について詳しく解説します。Squareを使った決済処理の導入を検討している方や、実装の参考にしたい方に役立つ内容となっています。
実装イメージ
購入ボタンをクリックするとSquareの決済画面に遷移します。
必要な情報を入力し「支払い」処理をすると、支払いが完了するというものになります。
クレジットカード番号の入力などは全てSquare側が提供している画面になっており、アプリケーションではカード番号などの情報は保持していません。
Squareとは?(ウェブ開発者向け)
Square は、オンライン・オフラインの決済処理を簡単に導入できる決済サービスです。POSシステムやEC決済、定期課金、APIを提供しており、開発者は柔軟にカスタマイズできます。
開発者視点での特徴
1️.多様な決済手段に対応
- クレジットカード(Visa, Mastercard, Amex, JCB など)
- デビットカード
- モバイル決済(Apple Pay, Google Pay)
- 電子請求書・定期課金
2.RESTful API が充実
- Payments API: クレカ決済・モバイル決済を処理
- Orders API: 注文管理が可能
- Invoices API: 電子請求書の発行・管理
- Subscriptions API: 定期課金のセットアップが可能
- Customers API: 顧客データを一元管理
👉 メリット: シンプルな REST API なので、バックエンド・フロントエンドどちらからも扱いやすい
3.サーバーレス・クラウド環境でも動作
- AWS Lambda や Firebase Functions でも利用可能
- Webhook で支払い完了をリアルタイム処理
👉 メリット: バックエンドの管理コストを削減できる
メリット: バックエンドの管理コストを削減できる
3.PCI DSS に準拠
- カード情報の保存・処理を Square 側で管理
- セキュリティ対策(トークン化、暗号化)が組み込み済み
👉 メリット: PCI DSS 対応の手間を削減できる
Square公式のNext.js連携ブログ
実はSquare公式ブログでNext.js(App Router)での実装例がブログとして紹介されています。
実装当初はこちらを使った方が早いと思ったのですが、若干の問題点が見つかり諦めました。
こちらの実装例ではNext.jsのクライアント画面でクレジットカードの入力を促すよう設定しています。
入力自体はコンポーネントやバリデーションを含んだライブラリを使っています。
これはこれでコンポーネントを使えば楽なのですが、
- クレジットカードを記憶して2回目以降も使いたい
- クレジットカード以外の決済をしたい
などの要件が控えていたこともあり見送りました。
素早くクレジットカード決済のみ実装したいのであれば上記の手段でも可能ですが、今回の要件ではマッチせず諦めました。
システム構成
Next.jsでフロントの画面を提供しており、データの保存先としてMySQL、決済処理としてSquareを使用しています。
基本的にはNext.jsのサーバーサイドでこれらのバックエンドサービスと連携しています。
決済処理についてはクレジットカードの入力画面をSquareが提供しているため、Squareからユーザーへ矢印が伸びています。
ER図
DBのER図になります。
実際のサービスでは数多くのテーブルを使用していますが、今回必要なのは下記の3テーブルとなります。
- Course: ユーザーが購入するコンテンツみたいなもの
- Purchase: ユーザーの購入履歴
- SquareCustomer: サービスが管理しているuserIdとSquareで管理するカスタマーIDを紐付けるテーブル
SquareCustomerについてはどのテーブルともリレーションがありません。
Squareにアクセスし必要な情報を取得しサービスと連携して何か情報提供する際にuserIdを変換するのみの役割となります。
処理フロー
詳細な実装の前に決済の開始から終了までの処理フローをまとめました。
-
購入ボタンのクリック
-
Next.jsのサーバーサイド(今回はAPIを使用)でリクエストを受け取り
リクエスト内容を受け取りバリデーションなどを必要に行う。 -
リクエストのあったユーザーについてSquareのカスタマーIDがあるかDBに問い合わせ
カスタマーIDが存在しない場合は、Squareに問い合わせて新規にカスタマーIDを発行し、DBに登録(SquareCustomerテーブル) -
Next.jsのサーバーサイドからSquareに支払い用画面のURL発行のリクエスト
-
Squareから返ってきた支払い用画面のURLをユーザーに渡す
-
ユーザーが支払い用画面でクレジットカード番号の入力など行い支払い処理
-
Squareで決済処理
-
決済が正常終了した場合に、Squareから Webhook で支払い完了通知
-
Next.jsのサーバーサイドでDBに購入履歴の登録
Purchaseテーブルを使用 -
ユーザーが購入されたコンテンツの閲覧
実装
上記の処理フローをベースにそれぞれの実装でどのようなコードになっているか補足します。
- 購入ボタンのクリック
こちらは購入画面の実装部分です。
getChapter
でDBにアクセスし該当のコースが購入済みか確認します。
購入前であれば、CourseEnrollButton
ボタンを表示します。
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>
次に購入ボタンの実装です。
購入ボタンをクリックすると、API(/api/courses/${courseId}/checkout
)にアクセスし、レスポンスからURLを取得し別ページで開くよう実装します。
URLをどのように生成しクライアントサイドに返しているかは「4. Next.jsのサーバーサイドからSquareに支払い用画面のURL発行のリクエスト」でご説明します。
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("エラーが発生しました");
} 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> : `販売価格 ${formatPrice(price)}`}
</Button>
)
}
- Next.jsのサーバーサイド(今回はAPIを使用)でリクエストを受け取り
API(/api/courses/${courseId}/checkout
)の具体的な実装部分になります。
今回のプロジェクトではClerkという認証サービスを使っておりcurrentUser
を使うとサーバーサイドでユーザー情報が取得できます。
それらを使い存在するユーザーか確認を行い、今回購入するコース(コンテンツ)が存在するかなどリクエストのバリデーションを行います。
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 });
}
- リクエストのあったユーザーについてSquareのカスタマーIDがあるかDBに問い合わせ
db.squareCustomer.findUnique
にて、SquareカスタマーIDの登録があるか確認をしています。
まだカスタマーIDが作成されていなければ、アクセスしたユーザーのEmailを使って、client.customersApi.createCustomer
にてSquareでカスタマーIDを作成します。
その後、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 || '',
}
});
}
- Next.jsのサーバーサイドからSquareに支払い用画面のURL発行のリクエスト
client.checkoutApi.createPaymentLink
で支払い用画面のURLを発行するようリクエストを出します。
支払い用画面でユーザーが支払い処理をした後にコンテンツの画面に戻るようにするために、checkoutOptions
を使ってリダイレクト先のURLを指定しています。
createPaymentLink() の概要
const response = await client.checkoutApi.createPaymentLink({...});
- client.checkoutApi.createPaymentLink() を使用し、Squareの決済リンクを作成。
- 購入者がリンクをクリックすると決済画面に遷移 し、支払いが完了するとSquareがWebhookを送信。
- response には 作成された決済リンクの情報(URLなど)が格納されます。
決済リクエストのパラメータ
idempotencyKey: リクエストの冪等性(べきとうせい)を保証
idempotencyKey: uuidv4(),
- uuidv4() を使用し、一意なID(UUID)を生成。
- 同じリクエストが複数回送信されても、決済が重複しないようにするための仕組み。
order: 注文情報
order: {
locationId: process.env.SQUARE_LOCATION_ID!,
customerId: squareCustomer.squareCustomerId,
lineItems: [...],
metadata: {...}
}
- locationId(店舗ID): Squareで設定した決済処理を行う場所(店舗)を指定。
- customerId: Squareに登録された顧客ID。
- lineItems: 購入する商品の詳細(名前、数量、価格など)。
- metadata: 追加情報(course_id など)を埋め込む(後で参照可能)。
lineItems: 購入アイテム情報
lineItems: [
{
name: course.title,
quantity: '1',
itemType: 'ITEM',
metadata: {
'course_id': course.id,
'user_id': user.id,
},
basePriceMoney: {
amount: BigInt(course.price),
currency: 'JPY'
}
}
]
各購入アイテムの詳細:
- name: 商品名(例: course.title)。
- quantity: 商品の購入数(この場合は 1)。
- itemType: 商品の種類("ITEM")。
- metadata: 商品ごとの追加情報(コースID、ユーザーID)。
- basePriceMoney: 商品の価格情報。
- amount: 金額(BigInt(course.price) で整数値を使用)。
- currency: 通貨(JPY = 日本円)。
checkoutOptions: 決済後のリダイレクトURL
checkoutOptions: {
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/courses/${course.id}/chapters/${course.chapters[0].id}`
}
- 決済完了後にリダイレクトされるURL を指定。
- 購入者がコンテンツを購入した後、最初のチャプター(course.chapters[0])のページに移動 するよう設定しています。
prePopulatedData: 事前入力データ
prePopulatedData: {
buyerEmail: user.emailAddresses[0].emailAddress,
}
- 購入者のメールアドレスを事前入力。
- Squareの決済画面で、ユーザーのメールアドレス欄に自動入力されます。
paymentNote: 支払いメモ
paymentNote: course.id
- 決済トランザクションにコースIDを紐付ける ためのメモ。
- Squareの管理画面などで、どのコースの決済かを簡単に識別できるようになります。
最後に全体のコードを載せておきます。
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
});
- Squareから返ってきた支払い用画面のURLをユーザーに渡す
クライアントサイドに返すレスポンスとしてSquareから受け取ったURLをセットします。
今回は分割してコードを書いていますが、実際は2〜5のコードがAPIの一連の処理となります。
return NextResponse.json({ url: response.result.paymentLink?.longUrl });
- ユーザーが支払い用画面でクレジットカード番号の入力など行い支払い処理
5で受け取ったURLで画面を開くと支払い処理が可能となっています。
こちらはSquare内部の処理となります。
-
Squareで決済処理
-
決済が正常終了した場合に、Squareから Webhook で支払い完了通知
支払い完了通知を受け取る処理を実装する前に、支払い完了通知を受け取るURLをSquareで設定します。
webhookの設定画面で通知先のURLを設定します。
合わせてどのEventを通知するかの設定も行う必要があり今回はpayment.craeted
とpayment.updated
を設定しています。
次に受け取るNext.jsのAPIの設定になります。
このAPIは、SquareのWebhookイベントを処理するためのエンドポイントを提供します。POSTメソッドを使用し、Squareから送信されるWebhookリクエストを受け取ります。以下に各処理の補足を記載します。
リクエストボディの取得
- req.text() を使用し、リクエストのボディ(JSON形式のデータ)を文字列として取得。
- Webhookのリクエストは application/json で送信されるため、文字列のまま扱うことで署名検証時の改変を防ぐ。
export async function POST(req: Request) {
const body = await req.text();
Squareの署名検証
const signature = headers().get("x-square-hmacsha256-signature") || "";
- SquareのWebhookリクエストには、x-square-hmacsha256-signature というヘッダーが含まれます。
- これは、Squareがリクエストの正当性を保証するために付与するHMAC署名。
- 署名が取得できない場合は、空文字 "" を代入(ただし、検証時にエラーとなります)
Webhookの署名検証
const result = WebhooksHelper.isValidWebhookEventSignature(
body,
signature,
SIGNATURE_KEY,
NOTIFICATION_URL
);
- WebhooksHelper.isValidWebhookEventSignature() を使って 署名の正当性を確認。
- SIGNATURE_KEY は SquareのWebhookシークレットキー(管理画面で設定)。
- NOTIFICATION_URL は このエンドポイントのURL(Square側で設定したWebhookの送信先)。
- 署名が正しくない場合、リクエストは不正と判断されます。
Webhookイベントのパース
const event = JSON.parse(body);
- 受信したリクエストボディ(JSON文字列)をパースし、JavaScriptオブジェクトに変換。
- Webhookのペイロードには type(イベント種別)や data(詳細データ)が含まれます。
受信データの分解
const { type, data } = event;
const { object } = data;
const { payment } = object;
const { note, customer_id, order_id, status } = payment;
- SquareのWebhookイベント構造に基づいてデータを展開。
- type には イベントの種類(例: "payment.created") が入ります。
- payment オブジェクト内に 決済情報(注文ID、顧客ID、メモ、決済ステータス など) が含まれます。
Webhookの処理条件
if (result && type === 'payment.created' && status === 'APPROVED') {
- result が true(署名検証OK)かつ、
- type が "payment.created"(支払いが新規作成されたイベント)かつ、
- status が "APPROVED"(承認済みの決済) の場合のみ処理を進める。
- これにより 不要なWebhookリクエストをフィルタリング できます。
必須データの確認
if (!customer_id || !note || !order_id) {
throw new Error('Webhook Error: Missing metadata');
}
- customer_id(Squareの顧客ID)、note(メモ情報)、order_id(注文ID)のいずれかが欠けていた場合、エラーをスロー。
- 不完全なデータで処理を進めないようにするためのバリデーションとなります。
SquareカスタマーIDを検索
const user = await db.squareCustomer.findUnique({
where: {
squareCustomerId: customer_id,
}
});
- Squareの customer_id をもとに データベース内のユーザー情報を検索。
- db.squareCustomer.findUnique() は、DBを使用して一意のユーザー情報を取得する処理となります。
顧客が存在しない場合のエラーハンドリング
if (!user) {
throw new Error('Webhook Error: User not found');
}
- 対応するユーザーが見つからなかった場合、エラーをスロー。
- これにより、存在しない顧客の情報を処理しないように防ぎます。
最後に全体のコードを載せておきます。
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');
}
- Next.jsのサーバーサイドでDBに購入履歴の登録
購入情報の保存
await db.purchase.create({
data: {
courseId: String(note),
userId: user.userId,
orderId: order_id,
}
});
- 支払いが承認されている場合、購入情報(コースID、ユーザーID、注文ID)をデータベースに保存します。
- note は支払いに関連するメタデータとして使われ、courseId に設定されます。
成功レスポンス
return new NextResponse(`Webhook Success: ${type}`, { status: 200 });
- Webhookイベントの処理が成功した場合、200 OKのレスポンスとともに、「Webhook Success: type」のメッセージを返します。
- ユーザーが購入されたコンテンツの閲覧
「4. Next.jsのサーバーサイドからSquareに支払い用画面のURL発行のリクエスト」のcheckoutOptionsで設定した決済後のリダイレクトURLでコンテンツの閲覧画面に遷移しますのでユーザーは視聴を開始できます。
さいごに
Squareを活用することで、セキュアかつ柔軟な決済システムを簡単に構築できることがわかりました。特に、クレジットカード情報をアプリケーション側で保持せずに済むため、セキュリティ対策の負担を大幅に軽減できる点は大きなメリットです。
また、Next.jsを用いたフロントエンドとバックエンドの連携を活用することで、決済処理のフローをスムーズに実装できました。Squareの多彩なAPIを組み合わせれば、今後さらに高度な決済機能を導入することも可能です。
本記事が、Squareを利用した決済システムの導入を考えている開発者の一助になれば幸いです。
関連する技術ブログ
Next.jsとAuth.jsで認証機能を実装するチュートリアル
2024/09/1310分で完成。AWS Amplify公式テンプレートを使ったNext.jsアプリの簡単デプロイ手順
2024/11/05Next.js × AWS CDK の統合環境構築:Docker でローカル開発から本番デプロイまで
2024/05/11Next.jsでのメール認証処理の実装ガイド:アカウント登録からトークン検証まで
2024/05/10Next.jsでのメール認証処理の実装ガイド:トークン検証からログイン画面へのリダイレクト処理までの詳細解説
2024/05/13Next.jsを活用したGitHubとGoogleのOAuth認証実装完全ガイド — スムーズなユーザーログインの実現方法
2024/06/11Next.jsでログイン画面を作ってメールアドレス/パスワードでログインできるようにする
2024/02/27Next.jsとmicroCMSで作るブログ:ヘッドレスCMSによるコンテンツ管理と表示
2024/12/16