🎣 Shopify Webhook設定 完全マニュアル
リアルタイム同期・PII分離・セキュリティ重視
Cloudflare Workers統合によるスケーラブルWebhook処理
🎯 Webhook システム概要
Webhook処理フロー
graph TB
A[Shopify Event] --> B[Webhook Trigger]
B --> C{Signature Verify}
C -->|❌ Invalid| D[401 Unauthorized]
C -->|✅ Valid| E[Event Router]
E --> F{Event Type}
F -->|orders/*| G[Order Handler]
F -->|customers/*| H[Customer Handler]
F -->|products/*| I[Product Handler]
G --> J[PII Filter]
H --> J
I --> J
J --> K[D1 Storage<br/>非PII のみ]
J --> L[Queue System]
L --> M[Factory Notification]
L --> N[Status Sync]
style C fill:#ffebcd,stroke:#ff6b6b,stroke-width:3px
style J fill:#e6ffe6,stroke:#00cc00,stroke-width:2px
📋 必要なWebhook一覧
注文関連Webhook
orders/create:
用途: 新規注文通知・工場システム連携
PII含有: ⚠️ 高(顧客情報・配送先)
処理: 非PII抽出→D1保存→工場通知
orders/updated:
用途: 注文ステータス更新
PII含有: ⚠️ 高(変更内容による)
処理: ステータスのみ抽出→D1更新
orders/paid:
用途: 決済完了通知・製作開始トリガー
PII含有: ⚠️ 中(決済情報)
処理: 決済ステータス→工場製作開始
orders/fulfilled:
用途: 配送完了通知・顧客通知
PII含有: ⚠️ 高(配送情報)
処理: 完了ステータス→顧客通知Queue
顧客関連Webhook
customers/create:
用途: Firebase UID連携・初期設定
PII含有: ⚠️ 最高(全顧客情報)
処理: Firebase UID抽出のみ→連携テーブル
customers/update:
用途: 設定変更・ブランド選好更新
PII含有: ⚠️ 高(更新内容による)
処理: 非PII設定のみ抽出→D1更新
商品関連Webhook
products/create:
用途: 新商品追加・カタログ更新
PII含有: ❌ なし
処理: 商品情報→D1保存→キャッシュ更新
products/update:
用途: 商品情報更新・価格変更
PII含有: ❌ なし
処理: 変更情報→D1更新→キャッシュ無効化
🔧 Webhook エンドポイント設定
1. Shopify管理画面での設定
1.1 Webhook作成手順
Step 1: 管理画面アクセス
URL: https://admin.shopify.com/store/[store-name]/settings/webhooks
Step 2: Webhook追加
Event: orders/create
Format: JSON
URL: https://api.contents-print.jp/webhooks/shopify/orders/create
API Version: 2024-01
Step 3: セキュリティ設定
Webhook signature: 有効
Secret: [自動生成されたSecret をメモ]
1.2 全Webhook設定一覧
| Event | URL | 説明 |
|---|---|---|
orders/create | /webhooks/shopify/orders/create | 新規注文 |
orders/updated | /webhooks/shopify/orders/updated | 注文更新 |
orders/paid | /webhooks/shopify/orders/paid | 決済完了 |
orders/fulfilled | /webhooks/shopify/orders/fulfilled | 配送完了 |
customers/create | /webhooks/shopify/customers/create | 新規顧客 |
customers/update | /webhooks/shopify/customers/update | 顧客更新 |
products/create | /webhooks/shopify/products/create | 新規商品 |
products/update | /webhooks/shopify/products/update | 商品更新 |
2. Cloudflare Workers実装
2.1 メインハンドラー実装
// workers/webhook-handler.ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const url = new URL(request.url);
const webhookHandler = new ShopifyWebhookHandler(env);
try {
return await webhookHandler.handle(request, url.pathname);
} catch (error) {
console.error('Webhook error:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
};
export class ShopifyWebhookHandler {
private env: Env;
private PIIFilter = new PIIFilter();
constructor(env: Env) {
this.env = env;
}
async handle(request: Request, path: string): Promise<Response> {
// 1. Webhook署名検証(最重要)
const isValid = await this.verifyWebhook(request);
if (!isValid) {
console.warn('Invalid webhook signature');
return new Response('Unauthorized', { status: 401 });
}
// 2. ヘッダーから情報取得
const topic = request.headers.get('x-shopify-topic');
const shopDomain = request.headers.get('x-shopify-shop-domain');
const requestId = request.headers.get('x-request-id');
console.log(`Webhook received: ${topic} from ${shopDomain}`);
// 3. ボディ取得
const body = await request.json();
// 4. イベントルーティング
return this.routeEvent(topic, body, requestId);
}
// Webhook署名検証(セキュリティ重要)
private async verifyWebhook(request: Request): Promise<boolean> {
const signature = request.headers.get('x-shopify-hmac-sha256');
if (!signature) return false;
const body = await request.text();
const secret = this.env.SHOPIFY_WEBHOOK_SECRET;
// HMAC-SHA256 検証
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const computedSignature = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(body)
);
const base64Signature = btoa(
String.fromCharCode(...new Uint8Array(computedSignature))
);
return signature === base64Signature;
}
// イベントルーティング
private async routeEvent(topic: string, data: any, requestId: string): Promise<Response> {
const metrics = {
topic,
timestamp: Date.now(),
request_id: requestId
};
try {
switch (topic) {
case 'orders/create':
await this.handleOrderCreate(data);
break;
case 'orders/updated':
await this.handleOrderUpdated(data);
break;
case 'orders/paid':
await this.handleOrderPaid(data);
break;
case 'orders/fulfilled':
await this.handleOrderFulfilled(data);
break;
case 'customers/create':
await this.handleCustomerCreate(data);
break;
case 'customers/update':
await this.handleCustomerUpdate(data);
break;
case 'products/create':
await this.handleProductCreate(data);
break;
case 'products/update':
await this.handleProductUpdate(data);
break;
default:
console.warn(`Unhandled webhook topic: ${topic}`);
}
// 成功メトリクス記録
await this.recordMetrics({ ...metrics, success: true });
return new Response('OK', { status: 200 });
} catch (error) {
console.error(`Error processing ${topic}:`, error);
// エラーメトリクス記録
await this.recordMetrics({ ...metrics, success: false, error: error.message });
// リトライ可能エラーの場合は500返却
return new Response('Processing Error', { status: 500 });
}
}
}
2.2 注文イベント処理
// handlers/order-handlers.ts
export class OrderWebhookHandlers {
private env: Env;
private PIIFilter: PIIFilter;
// 新規注文処理
async handleOrderCreate(order: ShopifyOrder): Promise<void> {
console.log(`Processing new order: ${order.id}`);
// PII除去(重要)
const nonPIIOrder = this.PIIFilter.sanitizeOrder(order);
// 必要な非PII情報のみ抽出
const orderData = {
shopify_order_id: order.id,
firebase_uid: this.extractFirebaseUID(order),
brand: this.extractBrand(order),
status: 'pending',
line_items_count: order.line_items.length,
total_price: order.total_price,
currency: order.currency,
created_at: order.created_at,
// PIIは含めない
};
// D1データベースに保存
await this.saveOrderToD1(orderData);
// 工場システムに通知(Queue経由)
await this.notifyFactorySystem({
type: 'new_order',
order_id: order.id,
firebase_uid: orderData.firebase_uid,
brand: orderData.brand,
items: order.line_items.map(item => ({
product_id: item.product_id,
variant_id: item.variant_id,
quantity: item.quantity,
// PII除去済み
}))
});
// 顧客通知(Queue経由)
await this.queueCustomerNotification({
type: 'order_confirmation',
firebase_uid: orderData.firebase_uid,
order_id: order.id
});
}
// 決済完了処理
async handleOrderPaid(order: ShopifyOrder): Promise<void> {
console.log(`Order paid: ${order.id}`);
// ステータス更新
await this.updateOrderStatus(order.id, 'paid');
// 製作開始通知
await this.notifyFactorySystem({
type: 'start_production',
order_id: order.id,
priority: this.calculatePriority(order)
});
}
// Firebase UID抽出
private extractFirebaseUID(order: ShopifyOrder): string | null {
// line_itemsのattributesから取得
for (const item of order.line_items) {
const firebaseUID = item.properties?.find(p => p.name === 'firebase_uid')?.value;
if (firebaseUID) return firebaseUID;
}
// metafieldsから取得
const metafield = order.metafields?.find(m => m.key === 'firebase_uid');
return metafield?.value || null;
}
// ブランド抽出
private extractBrand(order: ShopifyOrder): 'neko' | 'tokinoe' | null {
for (const item of order.line_items) {
const brand = item.properties?.find(p => p.name === 'brand')?.value;
if (brand && ['neko', 'tokinoe'].includes(brand)) {
return brand as 'neko' | 'tokinoe';
}
}
return null;
}
// D1データベース保存
private async saveOrderToD1(orderData: any): Promise<void> {
const stmt = this.env.DB.prepare(`
INSERT INTO orders (
shopify_order_id, firebase_uid, brand, status,
line_items_count, total_price, currency, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
await stmt.bind(
orderData.shopify_order_id,
orderData.firebase_uid,
orderData.brand,
orderData.status,
orderData.line_items_count,
orderData.total_price,
orderData.currency,
orderData.created_at,
new Date().toISOString()
).run();
}
// 工場システム通知
private async notifyFactorySystem(notification: any): Promise<void> {
await this.env.FACTORY_QUEUE.send(notification);
}
// 顧客通知Queue
private async queueCustomerNotification(notification: any): Promise<void> {
await this.env.NOTIFICATION_QUEUE.send(notification);
}
}
3. PII分離システム
3.1 PIIフィルター実装
// utils/pii-filter.ts
export class PIIFilter {
// PII除去対象フィールド
private static readonly PII_FIELDS = [
// 顧客情報
'email', 'first_name', 'last_name', 'phone',
// 住所情報
'address1', 'address2', 'city', 'province', 'country', 'zip',
'billing_address', 'shipping_address',
// 決済情報
'payment_details', 'billing_method',
// その他個人情報
'customer', 'note', 'note_attributes'
];
// Order PII除去
sanitizeOrder(order: any): any {
const sanitized = { ...order };
// 直接PIIフィールド削除
for (const field of PIIFilter.PII_FIELDS) {
delete sanitized[field];
}
// Line itemsの個人情報削除
if (sanitized.line_items) {
sanitized.line_items = sanitized.line_items.map(item => ({
product_id: item.product_id,
variant_id: item.variant_id,
quantity: item.quantity,
price: item.price,
// title, name等の商品情報は保持
title: item.title,
variant_title: item.variant_title,
// propertiesから非PII のみ抽出
properties: this.filterProperties(item.properties || [])
}));
}
return sanitized;
}
// Properties フィルタリング
private filterProperties(properties: any[]): any[] {
const allowedKeys = ['brand', 'firebase_uid', 'image_hash', 'processing_notes'];
return properties.filter(prop => allowedKeys.includes(prop.name));
}
// Customer PII除去
sanitizeCustomer(customer: any): any {
return {
id: customer.id,
created_at: customer.created_at,
updated_at: customer.updated_at,
orders_count: customer.orders_count,
total_spent: customer.total_spent,
state: customer.state,
// PIIは全て除去
};
}
}
4. エラーハンドリング・リトライ
4.1 エラー分類・処理
// utils/webhook-errors.ts
export class WebhookErrorHandler {
static async handleError(error: Error, context: WebhookContext): Promise<WebhookResponse> {
const errorType = this.classifyError(error);
switch (errorType) {
case 'SIGNATURE_INVALID':
// セキュリティエラー - リトライしない
console.error('Invalid webhook signature:', context);
return { status: 401, retry: false };
case 'DATABASE_ERROR':
// DB エラー - リトライ可能
console.error('Database error:', error, context);
await this.queueRetry(context, 1);
return { status: 500, retry: true };
case 'RATE_LIMIT':
// レート制限 - 少し待ってリトライ
console.warn('Rate limit hit:', context);
await this.queueRetry(context, 2);
return { status: 429, retry: true };
case 'PROCESSING_ERROR':
// 処理エラー - 内容によってリトライ判定
console.error('Processing error:', error, context);
if (context.retryCount < 3) {
await this.queueRetry(context, context.retryCount + 1);
return { status: 500, retry: true };
}
return { status: 500, retry: false };
default:
console.error('Unknown error:', error, context);
return { status: 500, retry: false };
}
}
private static classifyError(error: Error): string {
if (error.message.includes('signature')) return 'SIGNATURE_INVALID';
if (error.message.includes('database')) return 'DATABASE_ERROR';
if (error.message.includes('429')) return 'RATE_LIMIT';
return 'PROCESSING_ERROR';
}
private static async queueRetry(context: WebhookContext, retryCount: number): Promise<void> {
// 指数バックオフで遅延
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
setTimeout(async () => {
// リトライQueue に追加
await context.env.WEBHOOK_RETRY_QUEUE.send({
...context,
retryCount,
scheduledAt: Date.now() + delay
});
}, delay);
}
}
5. 監視・ログシステム
5.1 Webhook メトリクス
// utils/webhook-metrics.ts
export class WebhookMetrics {
static async record(event: WebhookMetricEvent): Promise<void> {
const metrics = {
timestamp: Date.now(),
topic: event.topic,
shop_domain: event.shopDomain,
processing_time: event.processingTime,
success: event.success,
error_type: event.errorType || null,
order_id: event.orderId || null,
customer_id: event.customerId || null
};
// D1に保存
await this.saveToD1(metrics);
// 外部監視システムに送信
await this.sendToDatadog(metrics);
}
private static async saveToD1(metrics: any): Promise<void> {
const stmt = env.DB.prepare(`
INSERT INTO webhook_metrics (
timestamp, topic, shop_domain, processing_time,
success, error_type, order_id, customer_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
await stmt.bind(
metrics.timestamp,
metrics.topic,
metrics.shop_domain,
metrics.processing_time,
metrics.success ? 1 : 0,
metrics.error_type,
metrics.order_id,
metrics.customer_id
).run();
}
// 成功率監視
static async getSuccessRate(hours: number = 24): Promise<number> {
const stmt = env.DB.prepare(`
SELECT
COUNT(*) as total,
SUM(success) as successful
FROM webhook_metrics
WHERE timestamp > ?
`);
const since = Date.now() - (hours * 60 * 60 * 1000);
const result = await stmt.bind(since).first();
return result.total > 0 ? (result.successful / result.total) * 100 : 0;
}
}
6. テスト・デバッグ
6.1 Webhook テストツール
// tools/webhook-tester.ts
export class WebhookTester {
// テスト用Webhook送信
static async sendTestWebhook(type: string, payload: any): Promise<void> {
const signature = await this.generateSignature(JSON.stringify(payload));
const response = await fetch('https://api.contents-print.jp/webhooks/shopify/orders/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Topic': type,
'X-Shopify-Shop-Domain': 'test.myshopify.com',
'X-Shopify-Hmac-Sha256': signature,
'X-Request-Id': `test-${Date.now()}`
},
body: JSON.stringify(payload)
});
console.log(`Test webhook response: ${response.status}`);
}
// テスト用署名生成
private static async generateSignature(body: string): Promise<string> {
const secret = 'test-webhook-secret';
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
// テストデータ生成
static generateTestOrder(): any {
return {
id: 'test-order-' + Date.now(),
created_at: new Date().toISOString(),
total_price: "29.99",
currency: "JPY",
line_items: [{
product_id: 'test-product-1',
variant_id: 'test-variant-1',
quantity: 1,
price: "29.99",
title: "Test Cat Photo Print",
properties: [
{ name: 'firebase_uid', value: 'test-firebase-uid' },
{ name: 'brand', value: 'neko' }
]
}]
};
}
}
✅ Webhook設定チェックリスト
Shopify管理画面設定
- orders/create Webhook作成
- orders/updated Webhook作成
- orders/paid Webhook作成
- orders/fulfilled Webhook作成
- customers/create Webhook作成
- customers/update Webhook作成
- products/create Webhook作成
- products/update Webhook作成
Cloudflare Workers実装
- Webhook署名検証実装
- イベントルーティング実装
- PII分離システム実装
- エラーハンドリング実装
- リトライ機能実装
監視・テスト
- メトリクス記録実装
- ログシステム設定
- テストツール作成
- エンドツーエンドテスト実行
セキュリティ
- 署名検証テスト
- PII分離テスト
- エラー情報漏洩チェック
📍 次のステップ: セキュリティ設定の実装に進んでください。
所要時間: 約4-5時間
難易度: 🔴 高(セキュリティ・PII分離重要)
文書作成日: 2025-08-23
最終更新: 2025-08-23
バージョン: 1.0 - PII分離特化版