Skip to main content

🔔 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✅ 設定済み🔥 BlockingCS地獄/webhooks/orders/fulfilled

🟡 Phase 2: Security-Critical(事故防止)

Webhook現在状況緊急度月間損失リスク設定URL
orders/updated✅ 設定済み🔒 Security写真混入事故/webhooks/orders/updated
orders/cancelled未設定🔥 Critical10-15万円/webhooks/orders/cancelled
app/uninstalled未設定🔒 Securityセキュリティ侵害/webhooks/app/uninstalled

🟢 Phase 3: Customer Experience(運用効率)

Webhook現在状況緊急度月間損失リスク設定URL
customers/create✅ 設定済み🟡 High機会損失/webhooks/customers/create
customers/update未設定🟡 High5万円/webhooks/customers/update
orders/refunded未設定🟡 High会計不整合/webhooks/orders/refunded

🟣 Phase 4: Product Management(カタログ管理)

Webhook現在状況緊急度月間損失リスク設定URL
products/update未設定🟡 Medium3万円/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/cancelledapp/uninstalledを今日中に追加し、残り3個も今週中に設定完了してください。

💰 経済効果: 全Webhook実装により月間20-30万円の損失リスクを回避し、工場効率30%向上が期待できます。


📍 次のアクション:

  1. 今すぐ: orders/cancelled + app/uninstalled 設定
  2. 今週: 残り3個のWebhook設定
  3. 来週: 監視・アラートシステム実装

文書作成日: 2025-08-24
最終更新: 2025-08-24
バージョン: 1.0 - 完全要件版