CONTENT
ここから
この章でわかること
この章では、Square Checkout APIを使って、商品詳細ページから決済ページへ進む導線を作ります。
第6章では、商品詳細ページと購入ボタンを作りました。
ただし、まだ購入ボタンを押しても、本当の決済ページにはつながっていません。
この章では、次の流れを完成させます。
商品詳細ページを見る
↓
購入ボタンを押す
↓
Next.jsのRoute Handlerへリクエストする
↓
Square Checkout APIで決済リンクを作る
↓
Squareの決済ページへ移動する
↓
決済完了後、自社サイトへ戻る
攻略本でいうと、この章は「ショップ画面から支払いの扉を開くステージ」です。
ここまでできると、最小構成のECとしてかなり形になります。
7.1 Squareホスト型決済の考え方
Squareホスト型決済とは、決済画面をSquare側に用意してもらう方式です。
自分のECサイト内にカード番号入力フォームを作るのではなく、購入者をSquareが用意した決済ページへ移動させます。
Square公式ドキュメントでは、Checkout APIはSquareがホストする決済ページのURLを取得し、購入者が商品やサービスの支払いをできるようにするAPIとして説明されています。(Square)
流れは次の通りです。
自社ECサイト
↓
購入ボタン
↓
Squareホスト型決済ページ
↓
支払い
↓
自社ECサイトの完了ページへ戻る
この方式では、購入者が実際にカード情報を入力する画面はSquare側です。
自社サイト側では、カード番号やセキュリティコードを直接扱いません。
なぜ初心者に向いているのか
理由は、実装範囲を小さくできるからです。
ECで一番慎重に扱うべきなのは、決済まわりです。
特にカード情報の入力画面を自作する場合、セキュリティや決済処理の理解が必要になります。
Squareホスト型決済を使えば、最初は次の部分を自作しなくて済みます。
カード番号入力欄
有効期限入力欄
セキュリティコード入力欄
決済画面の細かい制御
カード情報の安全な送信処理
そのため、最初のEC構築では、Squareホスト型決済から始めるのがおすすめです。
7.2 自前決済画面を作らないメリット
自前決済画面とは、自分のサイト内にカード番号入力フォームを置く方式です。
たとえば、次のような画面です。
カード番号
有効期限
セキュリティコード
名義人
支払いボタン
もちろん、SquareにはWeb Payments SDKやPayments APIがあり、自社サイト内で決済入力を行う構成も可能です。
しかし、初心者が最初に作るECでは、まず自前決済画面を作らないほうが安全です。
SquareのAPIリファレンスでも、Checkout APIは事前構築済みのSquareホスト型チェックアウトページを通じて支払いを受け付けられ、フロントエンド実装なしでも組み込める方式として説明されています。(Square)
メリット1:実装量が少ない
自前決済画面を作る場合、カード入力UI、入力チェック、トークン化、支払い作成処理などを理解する必要があります。
Squareホスト型決済なら、まずは決済リンクを作るだけで進められます。
自前決済画面
↓
実装することが多い
Squareホスト型決済
↓
決済リンクを作って移動させる
メリット2:決済情報を直接扱わなくてよい
自社サイト側でカード番号を直接扱わない構成にできます。
これは、小規模ECでは大きな安心材料です。
メリット3:販売開始までが早い
最初の目的は、完璧なECを作ることではありません。
まずは、1件目の注文を受けられる状態にすることです。
商品ページ
↓
購入ボタン
↓
Square決済ページ
この導線を先に完成させます。
攻略ポイント
最初から難しい決済画面を作らない。
まずはSquareに決済ページを任せる。
これが、この章の攻略方針です。
7.3 CreatePaymentLinkの基本
Square Checkout APIで重要になるのが、CreatePaymentLinkです。
CreatePaymentLinkは、Squareホスト型の決済ページを作成するAPIです。
Square公式APIリファレンスでは、POST /v2/online-checkout/payment-linksはSquareホスト型チェックアウトページを作成し、アプリケーションは作成されたPayment Linkを購入者に共有して支払いを受け付けられると説明されています。(Square)
簡単に言うと、次のような役割です。
商品情報と金額をSquareへ送る
↓
Squareが決済ページを作る
↓
決済ページURLが返ってくる
↓
そのURLへ購入者を移動させる
必要になる情報
最小構成では、次のような情報が必要です。
商品名
価格
通貨
数量
Location ID
リダイレクト先URL
重要な考え方
購入ボタンから送るのは、価格ではなく商品IDにします。
悪い例です。
ブラウザから価格を送る
↓
危険
よい例です。
ブラウザから商品IDを送る
↓
サーバー側で商品IDから正しい価格を取得する
ブラウザ側の値は、改ざんされる可能性があります。
そのため、価格は必ずサーバー側で決めます。
Client Component
↓
productIdだけ送る
Route Handler
↓
商品データから価格を取得する
Squareへ正しい金額を送る
攻略ポイント
決済金額は、必ずサーバー側で決めます。
購入者のブラウザから送られた価格を、そのまま信用してはいけません。
7.4 Next.js Route HandlerからSquare APIを呼び出す
Square APIは、Next.jsのRoute Handlerから呼び出します。
Route Handlerとは、Next.jsのappディレクトリ内にAPIのような処理を作る仕組みです。
Next.js公式ドキュメントでは、Route Handlersはroute.tsまたはroute.jsを使って、Web標準のRequestとResponse APIでカスタムリクエストハンドラーを作れる仕組みとして説明されています。(Next.js)
今回は、次のファイルを作ります。
src
└─ app
└─ api
└─ checkout
└─ route.ts
購入ボタンから/api/checkoutへPOSTします。
Route Handler側では、商品IDを受け取り、商品データを確認し、Square APIへ決済リンク作成を依頼します。
環境変数を確認する
.env.localには、次の値が必要です。
SQUARE_ENVIRONMENT=sandbox
SQUARE_ACCESS_TOKEN=SandboxのAccess Token
SQUARE_LOCATION_ID=SandboxのLocation ID
NEXT_PUBLIC_APP_URL=http://localhost:3000
SQUARE_ACCESS_TOKENは、絶対にClient Componentで使いません。
サーバー側のRoute Handlerだけで使います。
Square APIを呼び出すRoute Handler
以下は、学習用の最小構成です。
import { NextResponse } from 'next/server';
import { getProductById } from '@/data/products';
type CheckoutRequestBody = {
productId: string;
};
type SquareCreatePaymentLinkResponse = {
payment_link?: {
url?: string;
};
};
/**
* 役割: unknown型のリクエスト本文がCheckoutRequestBodyか確認する
* 入力: JSON.parse後のunknown値
* 出力: productIdを持つリクエスト本文かどうか
*/
function isCheckoutRequestBody(value: unknown): value is CheckoutRequestBody {
if (typeof value !== 'object' || value === null) {
return false;
}
return (
'productId' in value &&
typeof (value as { productId: unknown }).productId === 'string'
);
}
/**
* 役割: Square APIのレスポンスに決済URLが含まれているか確認する
* 入力: Square APIから返るunknown値
* 出力: payment_link.urlを持つレスポンスかどうか
*/
function hasPaymentLinkUrl(
value: unknown,
): value is SquareCreatePaymentLinkResponse {
if (typeof value !== 'object' || value === null) {
return false;
}
const paymentLink = (value as { payment_link?: unknown }).payment_link;
if (typeof paymentLink !== 'object' || paymentLink === null) {
return false;
}
return typeof (paymentLink as { url?: unknown }).url === 'string';
}
/**
* 役割: 購入ボタンから送られた商品IDをもとにSquare決済リンクを作成する
* 入力: productIdを含むPOSTリクエスト
* 出力: Squareの決済ページURL
*/
export async function POST(request: Request) {
const accessToken = process.env.SQUARE_ACCESS_TOKEN;
const locationId = process.env.SQUARE_LOCATION_ID;
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
const squareEnvironment = process.env.SQUARE_ENVIRONMENT;
if (!accessToken || !locationId || !appUrl || !squareEnvironment) {
return NextResponse.json(
{ message: '環境変数が設定されていません。' },
{ status: 500 },
);
}
const body: unknown = await request.json();
if (!isCheckoutRequestBody(body)) {
return NextResponse.json(
{ message: 'リクエスト形式が正しくありません。' },
{ status: 400 },
);
}
const product = getProductById(body.productId);
if (!product) {
return NextResponse.json(
{ message: '商品が見つかりません。' },
{ status: 404 },
);
}
if (!product.isAvailable) {
return NextResponse.json(
{ message: 'この商品は現在購入できません。' },
{ status: 409 },
);
}
const baseUrl =
squareEnvironment === 'production'
? 'https://connect.squareup.com'
: 'https://connect.squareupsandbox.com';
const squareResponse = await fetch(
`${baseUrl}/v2/online-checkout/payment-links`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Square-Version': '2026-05-20',
},
body: JSON.stringify({
idempotency_key: crypto.randomUUID(),
quick_pay: {
name: product.name,
price_money: {
amount: product.price,
currency: product.currency,
},
location_id: locationId,
},
checkout_options: {
redirect_url: `${appUrl}/thank-you`,
},
}),
},
);
const squareData: unknown = await squareResponse.json();
if (!squareResponse.ok) {
return NextResponse.json(
{ message: 'Square決済リンクの作成に失敗しました。' },
{ status: 502 },
);
}
if (!hasPaymentLinkUrl(squareData)) {
return NextResponse.json(
{ message: 'Square決済ページURLを取得できませんでした。' },
{ status: 502 },
);
}
return NextResponse.json({
checkoutUrl: squareData.payment_link.url,
});
}
商品IDで取得する関数
src/data/products.tsに、IDで商品を取得する関数を追加します。
/**
* 役割: 商品IDから該当する商品を取得する
* 入力: 商品ID
* 出力: 一致するProduct。存在しない場合はundefined
*/
export function getProductById(productId: string): Product | undefined {
return products.find((product) => product.id === productId);
}
金額の単位に注意する
Squareのprice_money.amountは、最小通貨単位で指定します。
日本円の場合は、小数がないため、1200は1,200円として扱います。
price: 1200
↓
1,200円
通貨によって扱いが変わるため、本書ではまずJPYに固定して進めます。
攻略ポイント
Route Handlerは、Square APIへつなぐ門番です。
ここで次の確認を行います。
商品IDは正しいか
商品は存在するか
販売中か
価格はサーバー側の値か
環境変数は設定されているか
SquareからURLが返ったか
購入ボタンから直接Squareへ行かず、必ずRoute Handlerを通します。
7.5 購入ボタンから決済リンクへ遷移する
第6章で作ったBuyButtonは、/api/checkoutへPOSTする設計にしていました。
この章でRoute Handlerを作ったので、購入ボタンからSquare決済ページへ移動できるようになります。
購入ボタンの流れは次の通りです。
購入ボタンを押す
↓
/api/checkoutへproductIdを送る
↓
checkoutUrlが返る
↓
window.location.hrefでSquare決済ページへ移動
BuyButtonの重要部分を確認します。
const response = await fetch('/api/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ productId }),
});
レスポンスからcheckoutUrlを受け取ります。
window.location.href = data.checkoutUrl;
これで、購入者はSquare決済ページへ移動します。
処理中表示を入れる
購入ボタンを押してから決済ページへ移動するまで、少し時間がかかることがあります。
その間、何も表示が変わらないと、ユーザーが何度もボタンを押してしまう可能性があります。
そのため、処理中はボタンを無効化します。
<button
type="button"
onClick={handleClick}
disabled={!isAvailable || isLoading}
>
{isLoading ? '決済ページを準備中...' : '購入する'}
</button>
攻略ポイント
購入ボタンは、二度押し対策が必要です。
1回押した
↓
処理中にする
↓
もう押せないようにする
決済リンクの二重作成を防ぐためにも、UI側で処理中表示を入れます。
ただし、最終的な二重処理対策はサーバー側でも必要です。
7.6 redirect_urlの設定
redirect_urlは、Square決済が終わった後に戻るページです。
本書では、次のURLへ戻すことにします。
http://localhost:3000/thank-you
本番では、次のようになります。
https://example.com/thank-you
Route Handlerでは、次のように設定しています。
checkout_options: {
redirect_url: `${appUrl}/thank-you`,
},
appUrlは環境変数から取得します。
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
なぜ環境変数にするのか
開発環境と本番環境では、URLが違うからです。
開発環境
↓
http://localhost:3000
本番環境
↓
https://example.com
コードに直接URLを書くと、本番公開時に修正漏れが起きやすくなります。
環境変数で切り替えます。
NEXT_PUBLIC_APP_URL=http://localhost:3000
Vercel本番環境では、次のように設定します。
NEXT_PUBLIC_APP_URL=https://example.com
注意点
redirect_urlへ戻ってきたことだけで、決済完了と判断してはいけません。
ユーザーの画面遷移は信用しきれないからです。
完了ページへ戻ってきた
↓
決済完了の可能性がある
Webhookで支払い完了を確認した
↓
注文確定として扱う
本格的には、第12章以降でWebhookを使って決済完了を確認します。
この章では、最小構成として完了ページへ戻る導線を作ります。
攻略ポイント
redirect_urlは、購入者を戻すための道案内です。
決済完了の証明書ではありません。
本当に支払いが完了したかどうかは、Webhookで確認します。
7.7 決済完了ページの作成
次に、決済完了ページを作ります。
src/app/thank-you/page.tsxを作成します。
import Link from 'next/link';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'ご注文ありがとうございます',
description: '決済手続きが完了しました。',
};
/**
* 役割: Square決済後に表示する完了ページ
* 入力: なし
* 出力: 購入者へ決済完了後の案内を表示するページ
*/
export default function ThankYouPage() {
return (
<main className="mx-auto flex min-h-[70vh] max-w-3xl flex-col items-center justify-center px-4 py-16 text-center">
<div className="space-y-6">
<p className="text-sm font-semibold text-gray-500">Thank you</p>
<h1 className="text-3xl font-bold tracking-tight text-gray-900">
ご注文ありがとうございます
</h1>
<p className="leading-8 text-gray-700">
決済手続きが完了しました。
ご注文内容を確認のうえ、発送準備を進めます。
</p>
<p className="text-sm leading-7 text-gray-500">
なお、正式な注文確定は決済情報の確認後に行われます。
確認メールが届くまで、しばらくお待ちください。
</p>
<div className="flex justify-center gap-4">
<Link
href="/products"
className="rounded-md border border-gray-300 px-5 py-3 text-sm font-semibold text-gray-900"
>
商品一覧へ戻る
</Link>
<Link
href="/"
className="rounded-md bg-black px-5 py-3 text-sm font-semibold text-white"
>
トップページへ戻る
</Link>
</div>
</div>
</main>
);
}
完了ページで伝えること
完了ページでは、次の情報を伝えます。
購入のお礼
決済手続きが完了したこと
注文確認メールの案内
商品一覧やトップページへのリンク
ただし、この段階ではWebhookによる注文確定処理が未実装です。
そのため、表現は少し慎重にします。
強すぎる表現
↓
ご注文が確定しました
慎重な表現
↓
決済手続きが完了しました
攻略ポイント
完了ページは、購入後の安心感を作るページです。
支払いできたのか
次に何が起こるのか
メールは届くのか
問い合わせは必要か
購入者が不安にならないように、次の流れを伝えます。
7.8 決済キャンセル時の導線
購入者がSquare決済ページで支払いをやめる場合もあります。
そのため、キャンセル時の導線も考えておきます。
Square Checkoutの細かい戻り先挙動は設定や決済フローによって変わる可能性があるため、本書の最小構成では、まず自社サイト側にキャンセル案内ページを用意する方針にします。
決済をやめた
↓
商品ページへ戻る
↓
再度購入できる
src/app/checkout/cancel/page.tsxを作ります。
import Link from 'next/link';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '決済を中断しました',
description: '決済手続きは完了していません。',
};
/**
* 役割: 決済を中断した購入者向けの案内ページ
* 入力: なし
* 出力: 決済未完了であることと再購入導線を表示するページ
*/
export default function CheckoutCancelPage() {
return (
<main className="mx-auto flex min-h-[70vh] max-w-3xl flex-col items-center justify-center px-4 py-16 text-center">
<div className="space-y-6">
<p className="text-sm font-semibold text-gray-500">Canceled</p>
<h1 className="text-3xl font-bold tracking-tight text-gray-900">
決済を中断しました
</h1>
<p className="leading-8 text-gray-700">
決済手続きは完了していません。
商品をご確認のうえ、必要であればもう一度お手続きください。
</p>
<Link
href="/products"
className="inline-flex rounded-md bg-black px-5 py-3 text-sm font-semibold text-white"
>
商品一覧へ戻る
</Link>
</div>
</main>
);
}
キャンセル時に大切なこと
キャンセルは失敗ではありません。
ユーザーが迷っているだけの可能性もあります。
そのため、強いエラー表現にしないほうが自然です。
悪い例
↓
決済に失敗しました
よい例
↓
決済を中断しました
購入者を責めず、商品一覧や商品詳細へ戻れる導線を用意します。
攻略ポイント
キャンセルページは、離脱を減らすための回復ポイントです。
決済をやめた
↓
終わりではない
商品一覧へ戻る
↓
再検討できる
ECでは、迷った人をやさしく戻す導線が大切です。
7.9 エラー時のユーザー表示
決済リンク作成時には、エラーが起きる可能性があります。
たとえば、次のようなケースです。
環境変数が設定されていない
Square Access Tokenが間違っている
Location IDが間違っている
商品が見つからない
商品が売り切れている
Square APIが一時的に失敗する
通信が切れる
エラー時には、ユーザーにわかりやすく伝える必要があります。
ただし、内部情報をそのまま表示してはいけません。
表示してよいメッセージ
購入処理を開始できませんでした。
時間をおいて再度お試しください。
この商品は現在購入できません。
商品情報を確認できませんでした。
表示してはいけないメッセージ
Access Tokenが無効です
Square APIの認証に失敗しました
SQUARE_LOCATION_IDがありません
Webhook Secretが違います
内部エラーは、ユーザーではなく開発者が確認する情報です。
Route Handler側のエラー
Route Handlerでは、ユーザーに返す情報を絞ります。
return NextResponse.json(
{ message: 'Square決済リンクの作成に失敗しました。' },
{ status: 502 },
);
Client Component側のエラー
購入ボタンでは、ユーザー向けに簡単な案内を出します。
alert('購入処理を開始できませんでした。時間をおいて再度お試しください。');
本格的には、alertではなく、画面内にエラーメッセージを表示するほうが自然です。
攻略ポイント
エラー表示は、情報を出しすぎないことが大切です。
ユーザー向け
↓
どうすればよいかを伝える
開発者向け
↓
ログで原因を確認する
秘密情報や内部構造を画面に出してはいけません。
7.10 最小構成ECの完成
ここまでで、最小構成のECが完成します。
できるようになった流れは次の通りです。
商品一覧を見る
↓
商品詳細を見る
↓
購入ボタンを押す
↓
Square決済ページへ移動する
↓
支払いを行う
↓
完了ページへ戻る
これは、ECの最初のゴールです。
まだ、本格的な注文管理やWebhook処理は入っていません。
しかし、商品を表示し、Squareの決済ページへつなげる導線はできました。
現時点でできること
商品一覧を表示する
商品詳細を表示する
販売中・売り切れを表示する
購入ボタンを押せる
Square決済リンクを作れる
Square決済ページへ移動できる
決済完了ページを表示できる
まだできないこと
Webhookで決済完了を確認する
注文データを自前DBに保存する
在庫を自動で減らす
注文確認メールを送る
管理画面で注文を見る
キャンセルや返金を管理する
これらは、次のステージで追加していきます。
攻略ポイント
ここで大切なのは、完璧ではなくても「売れる導線」が見えたことです。
最初のゴール
↓
1商品をSquare決済へつなげる
次のゴール
↓
注文、在庫、Webhookを整える
EC開発は、一気に全部作ると難しくなります。
小さく作り、動かしながら育てることが大切です。
この章のまとめ
この章では、Square Checkout APIを使って購入導線を作りました。
Squareホスト型決済を使うことで、自前のカード入力画面を作らずに、Squareの決済ページへ購入者を移動できます。
SquareのCheckout APIは、Squareホスト型の決済ページURLを取得し、購入者に支払いをしてもらうための仕組みです。(Square)
Next.js側では、Route Handlerを使って/api/checkoutを作りました。
Route Handlerは、appディレクトリ内でWeb RequestとResponse APIを使ってカスタム処理を作れる仕組みです。(Next.js)
この章で作った流れは、次の通りです。
BuyButton
↓
/api/checkoutへPOST
Route Handler
↓
商品IDを確認
販売状態を確認
Square APIへリクエスト
Square
↓
決済ページURLを返す
BuyButton
↓
Square決済ページへ移動
最小構成のECとしては、ここまでで大きな一歩です。
ただし、決済完了ページへ戻ってきたことだけで注文確定として扱ってはいけません。
本当に支払いが完了したかどうかは、Webhookで確認します。
次の章でやること
次の章では、カート機能を実装します。
この章では、1商品をそのままSquare決済へ送るシンプルな流れを作りました。
次は、複数商品をまとめて購入できるように、カートの考え方を整理します。
localStorage、Cookie、サーバー側DBの違いを理解しながら、最初に作るべきカート機能を攻略していきます。