Skip to main content

🎣 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設定一覧

EventURL説明
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分離特化版