🔔 Shopify Webhook完全要件・設定マニュアル
Contents Print印刷ビジネス専用設計
写真混入防止・マルチブランド・PII分離対応の包括的Webhook設計
🎯 システム概要
Contents Print特有の要件
ビジネス特性:
- 写真プリント: セキュリティ最重要(写真混入は法的問題)
- マルチブランド: neko・tokinoe・dog の統合管理
- PII分離: Shopify=PII保存、Cloudflare=非PII処理
- 工場連携: リアルタイム印刷指示・進捗管理
重要な制約:
- 写真UID埋め込みによる三点検証システム必須
- ブランド別処理ロジック(配送期間・商品仕様差異)
- GDPR準拠のデータ管理
- 40req/min API制限対策
📊 完全Webhook要件マトリクス
🔴 Phase 1: Business-Critical(事業停止リスク)
| Webhook | 現在状況 | 緊急度 | 月間損失リスク | 設定URL |
|---|---|---|---|---|
orders/create | ✅ 設定済み | 🔥 Blocking | 全売上停止 | /webhooks/orders/create |
orders/paid | ✅ 設定済み | 🔥 Blocking | 顧客満足度0% | /webhooks/orders/paid |
orders/fulfilled | ✅ 設定済み | 🔥 Blocking | CS地獄 | /webhooks/orders/fulfilled |
🟡 Phase 2: Security-Critical(事故防止)
| Webhook | 現在状況 | 緊急度 | 月間損失リスク | 設定URL |
|---|---|---|---|---|
orders/updated | ✅ 設定済み | 🔒 Security | 写真混入事故 | /webhooks/orders/updated |
orders/cancelled | ❌ 未設定 | 🔥 Critical | 10-15万円 | /webhooks/orders/cancelled |
app/uninstalled | ❌ 未設定 | 🔒 Security | セキュリティ侵害 | /webhooks/app/uninstalled |
🟢 Phase 3: Customer Experience(運用効率)
| Webhook | 現在状況 | 緊急度 | 月間損失リスク | 設定URL |
|---|---|---|---|---|
customers/create | ✅ 設定済み | 🟡 High | 機会損失 | /webhooks/customers/create |
customers/update | ❌ 未設定 | 🟡 High | 5万円 | /webhooks/customers/update |
orders/refunded | ❌ 未設定 | 🟡 High | 会計不整合 | /webhooks/orders/refunded |
🟣 Phase 4: Product Management(カタログ管理)
| Webhook | 現在状況 | 緊急度 | 月間損失リスク | 設定URL |
|---|---|---|---|---|
products/update | ❌ 未設定 | 🟡 Medium | 3万円 | /webhooks/products/update |
inventory_levels/update | ❌ 未設定 | 🟢 Low | 機会損失 | /webhooks/inventory/levels |
🚨 緊急追加必要Webhook詳細
1. orders/cancelled - 最重要
ビジネス影響
損失シナリオ:
- キャンセル率: 5% (月間100件想定)
- 材料ロス: 1件あたり1,500円
- 月間損失: 150,000円
- 年間損失: 180万円
工場影響:
- 印刷済み商品の廃棄
- 工場リソースの無駄遣い
- 在庫管理混乱
実装仕様
// orders/cancelled webhook処理
interface CancelledOrderWebhook {
id: string;
cancelled_at: string;
cancel_reason?: string;
financial_status: string;
}
async function handleOrderCancellation(webhook: CancelledOrderWebhook) {
// 1. 印刷キューから即座に削除
await printQueue.remove(webhook.id);
// 2. 工場システムに緊急停止指示
await factoryAPI.emergencyStop({
orderId: webhook.id,
reason: 'customer_cancellation'
});
// 3. 在庫復帰処理
await inventory.restore(webhook.id);
// 4. セキュリティ記録の削除(GDPR対応)
await security.cleanupPhotoRecords(webhook.id);
// 5. D1キャッシュ更新
await d1.updateOrderStatus(webhook.id, 'cancelled');
}
Shopify設定
Event: Order cancellation
URL: https://api.contents-print.jp/webhooks/shopify/orders/cancelled
Format: JSON
API Version: 2025-07
Secret: 417ac888673c93ec75f614903515ddcd952cba42a79e0392f5a2def0609e503f
2. app/uninstalled - セキュリティ最重要
セキュリティリスク
リスクシナリオ:
- アプリ削除後もAPI Key有効
- 顧客データへの不正アクセス継続
- GDPR違反(データ削除義務)
- 法的賠償リスク: 数千万円
対策効果:
- 即座のAPI無効化
- データアクセス遮断
- セキュリティ監査ログ
実装仕様
// app/uninstalled webhook処理
async function handleAppUninstalled(webhook: AppUninstalledWebhook) {
// 1. 緊急セキュリティアラート
await security.emergencyAlert({
type: 'APP_UNINSTALLED',
severity: 'CRITICAL',
shop: webhook.shop_domain,
timestamp: Date.now()
});
// 2. API Key即座無効化
await apiKeys.disable(webhook.shop_domain);
// 3. 全セッション終了
await sessions.terminateAll(webhook.shop_domain);
// 4. データ保持ポリシー開始
await gdpr.startRetentionCountdown(webhook.shop_domain);
// 5. 管理者にSMS緊急通知
await sms.send(ADMIN_PHONE, `SECURITY ALERT: App uninstalled from ${webhook.shop_domain}`);
}
3. customers/update - 配送精度
ビジネス影響
影響シナリオ:
- 住所変更検知漏れ: 月間20件
- 誤配送コスト: 1件2,500円
- 再配送コスト: 1件1,000円
- 月間損失: 70,000円
- 顧客満足度低下
実装仕様
// customers/update webhook処理
async function handleCustomerUpdate(webhook: CustomerUpdateWebhook) {
// 1. 進行中注文の配送先更新確認
const activeOrders = await getActiveOrders(webhook.id);
for (const order of activeOrders) {
if (order.status === 'processing') {
// 2. 住所変更の場合は工場に通知
if (addressChanged(webhook)) {
await factoryAPI.updateShippingAddress(order.id, webhook.default_address);
}
// 3. D1キャッシュ更新(非PII部分のみ)
await d1.updateCustomerPreferences({
firebase_uid: order.firebase_uid,
brand_preference: extractBrandPreference(webhook),
communication_preference: webhook.accepts_marketing
});
}
}
}
4. orders/refunded - 会計整合性
ビジネス影響
会計リスク:
- 返金処理の非同期性
- 在庫数の不整合
- 売上レポートの誤差
- 税務申告への影響
実装仕様
// orders/refunded webhook処理
async function handleOrderRefunded(webhook: RefundedOrderWebhook) {
// 1. 返金額の検証
const refundAmount = webhook.refunds.reduce((sum, refund) => sum + refund.amount, 0);
// 2. 在庫復帰処理
await inventory.restoreFromRefund(webhook.order_id, webhook.line_items);
// 3. 会計システム連携
await accounting.recordRefund({
orderId: webhook.order_id,
amount: refundAmount,
reason: webhook.refunds[0].note,
timestamp: webhook.refunds[0].created_at
});
// 4. ブランド別売上統計更新
const brand = extractBrand(webhook);
await analytics.updateBrandRevenue(brand, -refundAmount);
}
5. products/update - 商品同期
ビジネス影響
価格リスク:
- 価格変更の反映遅延
- 古い価格での受注
- 商品仕様の不整合
- ブランド別価格差の混乱
実装仕様
// products/update webhook処理
async function handleProductUpdate(webhook: ProductUpdateWebhook) {
// 1. ブランド判定
const brand = webhook.metafields.find(m => m.namespace === 'nekomata' && m.key === 'brand');
// 2. D1商品キャッシュ更新
await d1.updateProduct({
id: webhook.id,
brand: brand?.value,
price: webhook.variants[0].price,
inventory: webhook.variants[0].inventory_quantity,
specifications: extractSpecs(webhook)
});
// 3. CDN無効化(フロントエンド即座反映)
await cdn.purge([
`/products/${webhook.handle}`,
`/api/products/brand/${brand?.value}`
]);
// 4. 価格変更アラート(大幅変更時)
const priceChange = calculatePriceChange(webhook);
if (Math.abs(priceChange) > 0.1) {
await alerts.send({
type: 'PRICE_CHANGE',
product: webhook.title,
change: priceChange
});
}
}
🔧 Webhook設定実装手順
Step 1: Partner Dashboard設定
必須追加Webhook(5個)
# Partner Dashboard > Contents Print Integration > Webhooks
1. orders/cancelled
URL: https://api.contents-print.jp/webhooks/shopify/orders/cancelled
2. app/uninstalled
URL: https://api.contents-print.jp/webhooks/shopify/app/uninstalled
3. customers/update
URL: https://api.contents-print.jp/webhooks/shopify/customers/update
4. orders/refunded
URL: https://api.contents-print.jp/webhooks/shopify/orders/refunded
5. products/update
URL: https://api.contents-print.jp/webhooks/shopify/products/update
共通設定
Format: JSON
API Version: 2025-07
Secret: 417ac888673c93ec75f614903515ddcd952cba42a79e0392f5a2def0609e503f
Step 2: Cloudflare Workers実装
メインWebhookハンドラー
// workers/webhook-handler.ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// セキュリティ検証
const isValid = await verifyShopifyWebhook(request, env.SHOPIFY_WEBHOOK_SECRET);
if (!isValid) {
return new Response('Unauthorized', { status: 401 });
}
const topic = request.headers.get('X-Shopify-Topic');
const payload = await request.json();
// ブランド判定(全Webhookで必要)
const brand = extractBrand(payload);
try {
switch (topic) {
// 既存Webhook
case 'orders/create':
return await handleOrderCreate(payload, brand, env);
case 'orders/paid':
return await handleOrderPaid(payload, brand, env);
case 'orders/fulfilled':
return await handleOrderFulfilled(payload, brand, env);
case 'orders/updated':
return await handleOrderUpdated(payload, brand, env);
case 'customers/create':
return await handleCustomerCreate(payload, brand, env);
// 🚨 新規追加必要Webhook
case 'orders/cancelled':
return await handleOrderCancelled(payload, brand, env);
case 'app/uninstalled':
return await handleAppUninstalled(payload, env);
case 'customers/update':
return await handleCustomerUpdate(payload, brand, env);
case 'orders/refunded':
return await handleOrderRefunded(payload, brand, env);
case 'products/update':
return await handleProductUpdate(payload, brand, env);
default:
console.warn(`⚠️ Unhandled webhook: ${topic}`);
return new Response('OK', { status: 200 });
}
} catch (error) {
console.error(`❌ Webhook error [${topic}]:`, error);
// 重要Webhookはリトライキューに送信
if (CRITICAL_WEBHOOKS.includes(topic)) {
await env.WEBHOOK_RETRY_QUEUE.send({
topic,
payload,
error: error.message,
timestamp: Date.now()
});
}
return new Response('Internal Error', { status: 500 });
}
}
};
const CRITICAL_WEBHOOKS = [
'orders/create',
'orders/paid',
'orders/cancelled',
'app/uninstalled'
];
セキュリティ・ブランド共通処理
// utils/webhook-common.ts
// Shopify署名検証
async function verifyShopifyWebhook(request: Request, secret: string): Promise<boolean> {
const signature = request.headers.get('X-Shopify-Hmac-Sha256');
const body = await request.clone().text();
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const calculatedSignature = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
const calculatedBase64 = btoa(String.fromCharCode(...new Uint8Array(calculatedSignature)));
return signature === calculatedBase64;
}
// ブランド判定(全Webhookで使用)
function extractBrand(payload: any): 'neko' | 'tokinoe' | 'dog' | null {
// 注文系Webhook
if (payload.metafields) {
const brandField = payload.metafields.find(
(mf: any) => mf.namespace === 'nekomata' && mf.key === 'brand'
);
return brandField?.value || null;
}
// 商品系Webhook
if (payload.product?.metafields) {
const brandField = payload.product.metafields.find(
(mf: any) => mf.namespace === 'nekomata' && mf.key === 'brand'
);
return brandField?.value || null;
}
return null;
}
// Firebase UID抽出(セキュリティ用)
function extractFirebaseUID(payload: any): string | null {
if (payload.metafields) {
const uidField = payload.metafields.find(
(mf: any) => mf.namespace === 'nekomata' && mf.key === 'firebase_uid'
);
return uidField?.value || null;
}
return null;
}
Step 3: D1データベーススキーマ
Webhook監視テーブル
-- webhook監視・ログテーブル
CREATE TABLE webhook_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL,
shop_domain TEXT NOT NULL,
webhook_id TEXT NOT NULL,
brand TEXT,
firebase_uid TEXT,
status TEXT NOT NULL, -- success, failed, retrying
error_message TEXT,
processing_time_ms INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_topic (topic),
INDEX idx_status (status),
INDEX idx_brand (brand),
INDEX idx_created_at (created_at)
);
-- セキュリティイベントテーブル
CREATE TABLE security_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL, -- app_uninstalled, invalid_signature, etc.
shop_domain TEXT NOT NULL,
severity TEXT NOT NULL, -- critical, high, medium, low
details TEXT, -- JSON
resolved BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_event_type (event_type),
INDEX idx_severity (severity),
INDEX idx_resolved (resolved)
);
📊 運用監視・アラート設計
KPI監視項目
Webhook成功率:
目標: >99.5%
アラート閾値: <95%
処理時間:
目標: <2秒
アラート閾値: >5秒
セキュリティイベント:
app/uninstalled: 即座アラート
署名検証失敗: 1時間以内対応
ビジネス影響:
orders/cancelled処理遅延: 即座アラート(材料ロス)
orders/paid処理失敗: 即座アラート(顧客満足度)
アラート通知設計
// monitoring/webhook-alerts.ts
interface WebhookAlert {
severity: 'critical' | 'high' | 'medium' | 'low';
webhook: string;
message: string;
action_required: string;
business_impact: string;
}
const ALERT_RULES = {
'orders/cancelled': {
failure_threshold: 1, // 1回失敗で即座アラート
notification: ['sms', 'email', 'slack'],
reason: '材料ロス防止のため'
},
'app/uninstalled': {
failure_threshold: 0, // 受信即座アラート
notification: ['sms', 'email', 'security-channel'],
reason: 'セキュリティ侵害防止'
},
'orders/paid': {
failure_threshold: 2,
notification: ['email', 'slack'],
reason: '顧客満足度維持'
}
};
🎯 実装優先度・タイムライン
緊急対応(今日中)
Priority 1 - Business Blocking:
❌ orders/cancelled: 材料ロス防止
❌ app/uninstalled: セキュリティ
所要時間: 4-6時間
責任者: 即座アサイン必要
重要対応(今週中)
Priority 2 - High Impact:
❌ customers/update: 配送精度
❌ orders/refunded: 会計整合性
❌ products/update: 商品同期
所要時間: 8-12時間
責任者: 開発チーム
品質向上(来月中)
Priority 3 - Quality:
📋 inventory_levels/update: 在庫管理
📋 checkouts/create: カート分析
所要時間: 4-8時間
責任者: プロダクトチーム
🚨 Critical Success Factors
1. セキュリティFirst
- 全Webhookで署名検証必須
- Firebase UID・ブランド検証
- 不正アクセス即座検知
2. ブランド分離
- 全Webhookでブランド判定
- ブランド別処理ロジック
- クロスブランド混入防止
3. PII分離遵守
- Shopify: PII保存
- Cloudflare: 非PII処理のみ
- GDPR準拠データ管理
4. 工場連携
- リアルタイム印刷指示
- 緊急停止機能
- 進捗監視システム
5. エラー回復
- Critical Webhook自動リトライ
- 失敗時の手動介入フロー
- データ整合性保証
📋 Webhook設定チェックリスト
設定完了確認
- orders/create: ✅ 設定済み
- orders/paid: ✅ 設定済み
- orders/updated: ✅ 設定済み
- orders/fulfilled: ✅ 設定済み
- customers/create: ✅ 設定済み
- orders/cancelled: ❌ 未設定 - 緊急必要
- app/uninstalled: ❌ 未設定 - 緊急必要
- customers/update: ❌ 未設定 - 重要
- orders/refunded: ❌ 未設定 - 重要
- products/update: ❌ 未設定 - 重要
テスト確認
- 署名検証テスト
- ブランド判定テスト
- エラーハンドリングテスト
- アラート通知テスト
- セキュリティ監視テスト
運用準備
- 監視ダッシュボード設定
- アラート通知設定
- エラー対応フロー文書化
- 緊急時対応手順作成
🎯 結論: 現在の5個では事業運営に重大なリスクがあります。最低でもorders/cancelledとapp/uninstalledを今日中に追加し、残り3個も今週中に設定完了してください。
💰 経済効果: 全Webhook実装により月間20-30万円の損失リスクを回避し、工場効率30%向上が期待できます。
📍 次のアクション:
- 今すぐ:
orders/cancelled+app/uninstalled設定 - 今週: 残り3個のWebhook設定
- 来週: 監視・アラートシステム実装
文書作成日: 2025-08-24
最終更新: 2025-08-24
バージョン: 1.0 - 完全要件版