CONTENT
ここから
この章でわかること
この章では、Square Webhookの基本を整理します。
第11章では、在庫管理を扱いました。
在庫を正しく減らすには、Squareで決済が完了したことを正しく知る必要があります。
そのために使うのがWebhookです。
Webhookは、Square側で何かが起きたときに、自分のNext.jsアプリへ通知してくれる仕組みです。
Squareで決済が更新される
↓
SquareからNext.jsへ通知が届く
↓
Next.jsで注文を確認する
↓
自前DBを更新する
↓
在庫を減らす
↓
メールを送る
攻略本でいうと、Webhookは「Squareから届く通知書」です。
決済完了、注文更新、在庫変更などのイベントを受け取り、自社ECの処理を進めるために使います。
12.1 Webhookとは何か
Webhookとは、あるサービスでイベントが発生したときに、別のシステムへ自動で通知する仕組みです。
Square公式ドキュメントでは、Webhookは在庫変更やPOS決済などのイベントについて、開発者がリアルタイム通知を受け取れる仕組みとして説明されています。(Square)
ECで考えると、Webhookは次のような場面で使います。
決済が完了した
注文が更新された
在庫が変更された
返金が発生した
キャンセルされた
Webhookがない場合、自分のシステムはSquare側の変化をすぐに知ることができません。
その場合、定期的にSquare APIへ問い合わせる必要があります。
Webhookなし
↓
こちらから何度も確認しに行く
Webhookあり
↓
Squareから通知してもらう
Webhookの基本イメージ
Square
↓
イベント発生
Square Webhook
↓
Next.jsのURLへPOST
Next.js Route Handler
↓
イベントを受信
自前DB
↓
注文や在庫を更新
攻略ポイント
Webhookは、Squareから自動で届く連絡です。
ユーザーが画面を開いているか
↓
関係ない
Square側でイベントが起きたか
↓
これが重要
購入者が完了ページを開かなくても、Webhookを受け取ればサーバー側で処理を進められます。
12.2 なぜ決済完了はフロントエンドで判断してはいけないのか
ECで非常に重要なのが、決済完了をフロントエンドだけで判断しないことです。
フロントエンドとは、購入者のブラウザ側の画面です。
たとえば、Square決済後に次のページへ戻ってきたとします。
/thank-you
このページが表示されたからといって、必ず支払いが完了したとは限りません。
フロントエンド判断が危険な理由
URLは直接開ける
ブラウザ操作は信用できない
通信途中で失敗することがある
決済ページから戻らないユーザーもいる
画面表示と決済完了は別物
たとえば、ユーザーが直接次のURLを開くこともできます。
https://example.com/thank-you
このページを開いただけで注文確定にしてしまうと危険です。
正しい判断方法
決済完了は、Square側の情報で判断します。
ユーザーが完了ページへ戻る
↓
購入者への案内として使う
Square Webhookで決済完了を受け取る
↓
注文確定処理に使う
Square Payments APIのWebhookでは、支払いが作成・更新されたときに通知を受け取れます。たとえば支払いが完了した場合、payment.updatedイベントが生成されると説明されています。(Square)
攻略ポイント
完了ページは、購入者向けの表示です。
Webhookは、サーバー向けの証拠です。
完了ページ
↓
ユーザーに安心してもらうため
Webhook
↓
注文を確定するため
注文確定、在庫減算、メール送信は、Webhookを基準に行います。
12.3 Square Webhookの基本
Square Webhookでは、Square側でイベントが発生したときに、自分のアプリのURLへHTTP POSTが送られます。
たとえば、次のようなURLを用意します。
https://example.com/api/square/webhook
Square Developer Dashboardで、このURLをWebhook通知先として登録します。
SquareのWebhook Events Referenceでは、Square APIの各イベントに購読できることが説明されています。(Square)
Webhookで受け取るもの
Webhookでは、主に次のような情報を受け取ります。
イベントID
イベント種別
発生日時
関連するSquareオブジェクトID
イベントデータ
イベント種別には、たとえば次のようなものがあります。
payment.updated
order.updated
inventory.count.updated
refund.updated
最初に見るべきイベント
ECの最小構成では、まず次のイベントを意識します。
payment.updated
↓
決済状態の更新を見る
order.updated
↓
Square Orderの更新を見る
ただし、最初からすべてのイベントを処理しようとしなくて大丈夫です。
まずは決済完了を安全に確認することを優先します。
攻略ポイント
Webhookは、全部を受け取るより、必要なイベントから始めます。
最初
↓
payment.updated
次の段階
↓
order.updated
さらに拡張
↓
在庫、返金、キャンセル
イベントを増やすほど、処理も複雑になります。
12.4 Next.js Route HandlerでWebhookを受ける
Next.jsでWebhookを受け取るには、Route Handlerを使います。
Next.js公式ドキュメントでは、Route Handlerはappディレクトリ内でroute.tsを使い、Web RequestとResponse APIによるカスタムリクエストハンドラーを作る仕組みとして説明されています。(Next.js)
ファイル構成は次のようにします。
src
└─ app
└─ api
└─ square
└─ webhook
└─ route.ts
最小のWebhook受信処理
まずは、受け取ったイベントを確認するだけのRoute Handlerを作ります。
import { NextResponse } from 'next/server';
/**
* 役割: Square Webhookを受け取る
* 入力: Squareから送られるWebhook POSTリクエスト
* 出力: 受信結果を表すJSONレスポンス
*/
export async function POST(request: Request) {
const rawBody = await request.text();
console.log('Square Webhook raw body:', rawBody);
return NextResponse.json({
received: true,
});
}
Webhook署名検証では、リクエスト本文の生データが重要になります。
そのため、最初にrequest.json()ではなく、request.text()で本文を文字列として取得する設計にします。
JSONとして読む場合
署名検証が終わった後に、JSONとして扱います。
const eventData: unknown = JSON.parse(rawBody);
攻略ポイント
Webhookでは、本文をすぐにrequest.json()で読まないほうが安全です。
raw bodyを取得
↓
署名検証
↓
JSON.parse
↓
イベント処理
この順番を覚えておきます。
12.5 Webhook署名検証の考え方
Webhook署名検証とは、本当にSquareから届いた通知かどうかを確認する処理です。
Square公式ドキュメントでは、Webhook通知を受け取ったアプリケーションは、その通知がSquareから送られたものか検証し、通知に含まれるデータを検証する必要があると説明されています。(Square)
なぜ署名検証が必要なのか
Webhook URLはインターネット上に公開されます。
つまり、URLを知っている人が、偽物のPOSTリクエストを送る可能性があります。
偽物のWebhook
↓
注文を支払い済みにする
↓
在庫を減らす
↓
メールを送る
これを防ぐために、署名を検証します。
署名検証に使うもの
SquareのWebhook署名検証では、次のような情報を使います。
Webhook Signature Key
通知先URL
リクエスト本文
Squareから送られる署名ヘッダー
署名が一致すれば、Squareから送られた可能性が高いと判断できます。
署名が一致しなければ、処理を止めます。
環境変数
.env.localに署名キーを入れます。
SQUARE_WEBHOOK_SIGNATURE_KEY=SquareのWebhook Signature Key
SQUARE_WEBHOOK_URL=http://localhost:3000/api/square/webhook
本番では、公開URLにします。
SQUARE_WEBHOOK_URL=https://example.com/api/square/webhook
署名検証のイメージ
import crypto from 'node:crypto';
/**
* 役割: Square Webhookの署名が正しいか検証する
* 入力: 署名キー、通知先URL、リクエスト本文、Square署名ヘッダー
* 出力: 署名が一致するかどうか
*/
export function verifySquareWebhookSignature({
signatureKey,
notificationUrl,
rawBody,
signatureHeader,
}: {
signatureKey: string;
notificationUrl: string;
rawBody: string;
signatureHeader: string;
}): boolean {
const hmac = crypto.createHmac('sha256', signatureKey);
hmac.update(notificationUrl + rawBody);
const expectedSignature = hmac.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signatureHeader),
);
}
実務では、Square公式SDKや公式サンプルに合わせて実装を確認します。
Webhook Route Handlerで使う
import { NextResponse } from 'next/server';
import { verifySquareWebhookSignature } from '@/lib/square/verifyWebhookSignature';
export async function POST(request: Request) {
const rawBody = await request.text();
const signatureHeader = request.headers.get('x-square-hmacsha256-signature');
const signatureKey = process.env.SQUARE_WEBHOOK_SIGNATURE_KEY;
const notificationUrl = process.env.SQUARE_WEBHOOK_URL;
if (!signatureHeader || !signatureKey || !notificationUrl) {
return NextResponse.json(
{ message: 'Webhook設定が不足しています。' },
{ status: 500 },
);
}
const isValidSignature = verifySquareWebhookSignature({
signatureKey,
notificationUrl,
rawBody,
signatureHeader,
});
if (!isValidSignature) {
return NextResponse.json(
{ message: 'Invalid signature' },
{ status: 401 },
);
}
const eventData: unknown = JSON.parse(rawBody);
return NextResponse.json({
received: true,
});
}
攻略ポイント
Webhook署名検証は、入口の門番です。
署名OK
↓
処理する
署名NG
↓
処理しない
注文確定や在庫減算の前に、必ず検証します。
12.6 イベント種別の整理
Webhookでは、イベント種別によって処理を分けます。
たとえば、payment.updatedとorder.updatedでは、見るべき内容が違います。
イベント種別の例
payment.updated
↓
支払い状態が更新された
order.updated
↓
注文情報が更新された
refund.updated
↓
返金情報が更新された
inventory.count.updated
↓
在庫数が更新された
SquareのWebhook Events Referenceでは、Square APIごとに多数のイベント種別が整理されています。(Square)
TypeScriptでイベントを整理する
最初は、必要なイベントだけ型として定義します。
export type SquareWebhookEventType =
| 'payment.updated'
| 'order.updated';
export type SquareWebhookEvent = {
event_id: string;
type: SquareWebhookEventType;
created_at: string;
data: {
type: string;
id: string;
object?: unknown;
};
};
イベント種別で処理を分ける
function handleSquareWebhookEvent(event: SquareWebhookEvent): void {
switch (event.type) {
case 'payment.updated':
console.log('Payment updated:', event.data.id);
return;
case 'order.updated':
console.log('Order updated:', event.data.id);
return;
default:
return;
}
}
攻略ポイント
最初からすべてのイベントに対応しないことです。
最初
↓
payment.updated
必要になったら
↓
order.updated
さらに必要なら
↓
refund、inventory
対応するイベントを増やすほど、テストも増えます。
12.7 payment.updatedをどう扱うか
ECの最小構成で重要なのが、payment.updatedです。
Squareのpayment.updatedイベントは、Paymentが更新されたときに発行されます。公式リファレンスでは、通常、payment.statusやcard_details.statusが、キャンセル、承認、完了などで更新されたときに発行されると説明されています。(Square)
Payments APIのWebhook説明でも、支払いが完了した場合、payment.updatedイベントが生成される例が示されています。(Square)
payment.updatedで見ること
payment.updatedでは、主に次を確認します。
Payment ID
Payment status
Order ID
金額
通貨
作成日時
更新日時
支払い完了の判断
支払いが完了しているかどうかは、Paymentのステータスを見ます。
COMPLETED
↓
支払い完了として扱う候補
CANCELED
↓
キャンセル
FAILED
↓
失敗
APPROVED
↓
承認済みだが、まだ完了ではない場合がある
実務では、Webhookの情報だけで即判断せず、必要に応じてSquare APIでPaymentを再取得して確認します。
payment.updatedを受信
↓
Payment IDを取得
↓
Square APIでPaymentを確認
↓
statusがCOMPLETEDなら注文をpaidへ更新
処理フロー
payment.updatedを受け取る
↓
署名検証する
↓
イベントIDの重複を確認する
↓
Payment IDを取り出す
↓
Square APIでPaymentを確認する
↓
自前DBの注文を探す
↓
支払い済みに更新する
↓
在庫を減らす
↓
確認メールを送る
攻略ポイント
payment.updatedは、決済完了処理の中心です。
ただし、受け取ったらすぐに在庫を減らすのではなく、次の順番で進めます。
署名検証
↓
重複確認
↓
Payment確認
↓
注文更新
↓
在庫減算
↓
メール送信
順番を守ることで、事故を減らせます。
12.8 order.updatedをどう扱うか
order.updatedは、Square Orderが更新されたときに使うイベントです。
Square Orders APIのWebhookリファレンスでは、Orders APIに関連するWebhookイベントが提供されています。(Square)
order.updatedで見ること
order.updatedでは、主に次の情報を見ます。
Square Order ID
Order state
更新時刻
明細情報
fulfillment情報
payment.updatedとの違い
payment.updatedは支払い状態を見るイベントです。
order.updatedは注文情報の更新を見るイベントです。
payment.updated
↓
お金の状態を見る
order.updated
↓
Square側の注文状態を見る
ECの注文確定では、まずpayment.updatedを中心に考えるほうがわかりやすいです。
そのうえで、Square Orderの状態変化も追いたい場合にorder.updatedを使います。
order.updatedが役立つ場面
Square側の注文状態を同期したい
POS側の注文更新を自前DBに反映したい
fulfillment情報を追いたい
Square Dashboard側の変更を検知したい
注意点
payment.updatedとorder.updatedの両方を使う場合、同じ注文に対する処理が二重に走らないように注意します。
payment.updatedで在庫を減らした
↓
order.updatedでも在庫を減らす
↓
在庫が二重に減る
このような事故を防ぐために、イベントごとの責務を決めます。
payment.updated
↓
決済完了・注文確定・在庫減算の中心
order.updated
↓
Square Order状態の同期や補助情報の更新
攻略ポイント
order.updatedは便利ですが、最初から主役にしなくて大丈夫です。
最初の主役
↓
payment.updated
必要に応じて追加
↓
order.updated
注文確定処理をどのイベントで行うか、あらかじめ決めておきます。
12.9 Webhook受信ログを残す
Webhookを受け取ったら、ログを残します。
Webhookログは、後からトラブルを調べるために重要です。
Webhookが届いたか
何のイベントだったか
処理に成功したか
失敗したか
重複イベントだったか
webhook_eventsテーブルの例
create table webhook_events (
id uuid primary key default gen_random_uuid(),
provider text not null,
event_id text not null unique,
event_type text not null,
object_id text,
payload jsonb not null,
processing_status text not null,
error_message text,
received_at timestamptz not null default now(),
processed_at timestamptz
);
それぞれの意味は次の通りです。
| カラム | 意味 |
|---|---|
provider | squareなどの送信元 |
event_id | SquareのイベントID |
event_type | payment.updatedなど |
object_id | Payment IDやOrder ID |
payload | 受信したイベント本文 |
processing_status | 処理状態 |
error_message | 失敗時の内容 |
received_at | 受信日時 |
processed_at | 処理完了日時 |
processing_statusの例
received
processing
processed
failed
ignored
duplicate
ログを残す理由
Webhookが届いたか確認できる
同じイベントの重複を検知できる
失敗時に再処理しやすい
問い合わせ時に調査しやすい
本番障害の原因を追いやすい
攻略ポイント
Webhookログは、冒険の記録です。
問題が起きたときに、どこで失敗したのかを追えるようにします。
受信した
検証した
処理した
失敗した
重複だった
この記録があるだけで、運用の安心感が大きく変わります。
12.10 Webhookの再送と重複処理
Webhookは、同じイベントが複数回届く可能性があります。
SquareのWebhook概要では、通知の再試行があることが説明されています。受信側が正常に応答できない場合、通知が再送される可能性があります。(Square)
そのため、Webhook処理は「同じイベントがもう一度来ても壊れない」ように作ります。
この考え方を、冪等性と呼びます。
重複で起きる事故
同じpayment.updatedが2回届く
↓
注文確認メールが2通送られる
同じイベントを2回処理する
↓
在庫が2回減る
同じ注文を2回paidにする
↓
管理画面が混乱する
event_idで重複を防ぐ
Webhookイベントには、イベントIDがあります。
受信したイベントIDをDBに保存し、すでに処理済みなら再処理しません。
event_idを確認
↓
未登録なら処理する
↓
登録済みならduplicateとして終了
SQLでは、event_idに一意制約を付けます。
create unique index webhook_events_event_id_unique
on webhook_events(event_id);
処理済みチェックの流れ
Webhookを受信
↓
署名検証
↓
event_idを取得
↓
webhook_eventsに保存
↓
すでに存在したらduplicate
↓
未処理なら処理開始
↓
処理完了後にprocessedへ更新
Webhook Route Handlerの全体イメージ
import { NextResponse } from 'next/server';
import { verifySquareWebhookSignature } from '@/lib/square/verifyWebhookSignature';
type SquareWebhookEvent = {
event_id: string;
type: string;
created_at: string;
data: {
id: string;
type: string;
object?: unknown;
};
};
function isSquareWebhookEvent(value: unknown): value is SquareWebhookEvent {
if (typeof value !== 'object' || value === null) {
return false;
}
const candidate = value as {
event_id?: unknown;
type?: unknown;
created_at?: unknown;
data?: unknown;
};
if (
typeof candidate.event_id !== 'string' ||
typeof candidate.type !== 'string' ||
typeof candidate.created_at !== 'string' ||
typeof candidate.data !== 'object' ||
candidate.data === null
) {
return false;
}
const data = candidate.data as {
id?: unknown;
type?: unknown;
};
return typeof data.id === 'string' && typeof data.type === 'string';
}
/**
* 役割: Square Webhookを受信し、署名検証・イベント種別判定・重複対策を行う
* 入力: Squareから送られるWebhook POSTリクエスト
* 出力: 受信結果を表すJSONレスポンス
*/
export async function POST(request: Request) {
const rawBody = await request.text();
const signatureHeader = request.headers.get('x-square-hmacsha256-signature');
const signatureKey = process.env.SQUARE_WEBHOOK_SIGNATURE_KEY;
const notificationUrl = process.env.SQUARE_WEBHOOK_URL;
if (!signatureHeader || !signatureKey || !notificationUrl) {
return NextResponse.json(
{ message: 'Webhook設定が不足しています。' },
{ status: 500 },
);
}
const isValidSignature = verifySquareWebhookSignature({
signatureKey,
notificationUrl,
rawBody,
signatureHeader,
});
if (!isValidSignature) {
return NextResponse.json(
{ message: 'Invalid signature' },
{ status: 401 },
);
}
const parsedBody: unknown = JSON.parse(rawBody);
if (!isSquareWebhookEvent(parsedBody)) {
return NextResponse.json(
{ message: 'Invalid event payload' },
{ status: 400 },
);
}
const event = parsedBody;
/*
* 実務ではここで次の処理を行う。
*
* 1. webhook_eventsにevent_idを保存する
* 2. すでに存在する場合はduplicateとして終了する
* 3. event.typeごとに処理を分ける
* 4. 処理成功ならprocessedへ更新する
* 5. 失敗ならfailedへ更新する
*/
switch (event.type) {
case 'payment.updated':
console.log('payment.updated:', event.data.id);
break;
case 'order.updated':
console.log('order.updated:', event.data.id);
break;
default:
console.log('ignored event:', event.type);
break;
}
return NextResponse.json({
received: true,
});
}
攻略ポイント
Webhookは、1回だけ届く前提で作ってはいけません。
同じイベントが複数回届く
↓
普通にあり得る
同じイベントを複数回処理する
↓
事故になる
Webhook処理では、必ず重複チェックを入れます。
Webhook処理の完成イメージ
この章の内容をまとめると、Webhook処理は次のような流れになります。
Squareで決済が更新される
↓
SquareからWebhookが届く
↓
Next.js Route Handlerで受信
↓
raw bodyを取得
↓
署名検証
↓
JSONとして解析
↓
event_idを保存
↓
重複チェック
↓
event.typeで処理を分岐
↓
payment.updatedならPaymentを確認
↓
注文をpaidへ更新
↓
在庫を減らす
↓
メールを送る
↓
webhook_eventsをprocessedへ更新
Webhookは、注文処理の裏側で動く重要な仕組みです。
ユーザーには見えません。
しかし、ECの信頼性を支える中心になります。
この章のまとめ
この章では、Webhookの基礎を整理しました。
Webhookは、Square側でイベントが発生したときに、自分のNext.jsアプリへ通知してくれる仕組みです。
Square Webhookは、在庫変更やPOS決済などのイベントをリアルタイム通知として受け取れる仕組みとして提供されています。(Square)
この章で扱った内容は、次の通りです。
Webhookとは何か
なぜ決済完了をフロントエンドで判断してはいけないのか
Square Webhookの基本
Next.js Route HandlerでWebhookを受ける
Webhook署名検証の考え方
イベント種別の整理
payment.updatedをどう扱うか
order.updatedをどう扱うか
Webhook受信ログを残す
Webhookの再送と重複処理
特に重要なのは、次の3つです。
1. 決済完了はWebhookで確認する
2. Webhook署名を検証する
3. event_idで重複処理を防ぐ
完了ページへ戻ってきたことだけで、注文確定として扱ってはいけません。
完了ページ
↓
ユーザーへの案内
Webhook
↓
サーバー側の注文確定処理
Webhookを正しく扱うことで、注文確定、在庫減算、メール通知、管理画面更新を安全につなげられます。
次の章でやること
次の章では、決済完了後の注文処理を作ります。
この章では、Webhookを受け取るところまで整理しました。
次章では、payment.updatedを受け取った後に、SquareのPaymentを確認し、自前DBの注文をpaidへ更新し、在庫を減らし、メール通知へつなげる流れを攻略していきます。