目次
はじめに
Shopify の公式ドキュメントには「Webhook は at-least-once で配信される」と明記されている。つまり同じイベントが 2 回以上届く可能性がある。これを前提として設計していないと、以下の事故が起きる。
- 注文確定 Webhook が 2 回届き、WMS に注文が 2 件登録された
order/fulfilledイベントで顧客への発送通知メールが重複送信された- 在庫差し引きが 2 回実行されてマイナス在庫になった
Shopify は各 Webhook 配信に X-Shopify-Webhook-Id というユニーク ID を付与する。これを冪等キーとして使えば、重複処理を完全に防げる。
冪等性の実装パターン
基本構造
Shopify Webhook 受信
↓
X-Shopify-Webhook-Id を DynamoDB に記録(条件付き Put)
├── 成功(初回)→ 実際のビジネスロジックを実行
└── 失敗(重複)→ 200 を返してスキップ
200 を返すのが重要。5xx や 4xx を返すと Shopify がリトライを続けてしまう。
DynamoDB による重複排除
import { DynamoDBClient, PutItemCommand, ConditionalCheckFailedException } from "@aws-sdk/client-dynamodb"
const ddb = new DynamoDBClient({})
async function isFirstDelivery(webhookId: string): Promise<boolean> {
const ttl = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60 // 7日後に自動削除
try {
await ddb.send(new PutItemCommand({
TableName: "webhook-idempotency",
Item: {
pk: { S: `webhook:${webhookId}` },
ttl: { N: String(ttl) },
receivedAt: { S: new Date().toISOString() },
},
ConditionExpression: "attribute_not_exists(pk)",
}))
return true // 初回
} catch (error) {
if (error instanceof ConditionalCheckFailedException) {
return false // 重複
}
throw error
}
}Next.js API Route での実装例
// app/api/webhooks/shopify/orders-create/route.ts
import { headers } from "next/headers"
import crypto from "crypto"
export async function POST(request: Request) {
const body = await request.text()
const headersList = headers()
// 1. HMAC 署名検証
const hmac = headersList.get("x-shopify-hmac-sha256")
const expected = crypto
.createHmac("sha256", process.env.SHOPIFY_WEBHOOK_SECRET!)
.update(body)
.digest("base64")
if (hmac !== expected) {
return new Response("Unauthorized", { status: 401 })
}
// 2. 冪等性チェック
const webhookId = headersList.get("x-shopify-webhook-id")!
const isFirst = await isFirstDelivery(webhookId)
if (!isFirst) {
console.log(`Duplicate webhook skipped: ${webhookId}`)
return new Response("OK", { status: 200 }) // 200 を返してリトライを止める
}
// 3. ビジネスロジック
const order = JSON.parse(body)
await processNewOrder(order)
return new Response("OK", { status: 200 })
}HMAC 署名検証を最初に置く理由
冪等性チェック(DynamoDB への書き込み)より先に HMAC 検証を行う。順序を逆にすると、悪意のあるリクエストで DynamoDB にゴミデータが蓄積するリスクがある。
べき等キーの TTL を適切に設定する
DynamoDB の TTL は長すぎるとコストがかさみ、短すぎると Shopify のリトライ間隔(最長 48 時間)をカバーできない。7 日 を目安にすると Shopify の最大リトライ期間を十分にカバーできる。
冪等性だけでは足りないケース
冪等性チェックをパスした「初回処理」が途中で失敗した場合、次の再配信は「重複」と判定されてスキップされる。このとき処理が中途半端な状態で止まる。
対策は 2 フェーズ処理。
- DynamoDB に
status: "processing"で記録 - ビジネスロジックを実行
- 完了後に
status: "completed"に更新
再配信時に status: "processing" を見つけたら、前回の途中から再実行するリカバリーロジックを入れる。
まとめ
| リスク | 対策 |
|---|---|
| Webhook の重複配信 | DynamoDB 条件付き Put で排除 |
| 不正リクエスト | HMAC 検証を最初に実施 |
| 初回処理の中断 | 2 フェーズ処理 + ステータス管理 |
| DynamoDB コスト増大 | TTL で 7 日後に自動削除 |




