ECサイトの配送通知システム構築ガイド

「発送しました」というメール1通だけで終わっていませんか? 現代のEC利用者は、荷物がどこにあるのかをリアルタイムに知りたいと考えています。本チュートリアルでは、WhereParcel APIとWebhookを使って、ECサイトに本格的な配送通知システムを構築する方法をステップバイステップで解説します。

構築するシステムの全体像

本チュートリアルで構築するシステムは、以下の4つの機能を備えています:

  1. 自動追跡登録 — 注文が発送されたら、追跡番号を自動でWhereParcelに登録
  2. リアルタイムステータス更新 — Webhookで配送状況の変化を即座に受信
  3. メール・SMS通知 — 重要なマイルストーンで顧客に自動通知
  4. 顧客向け追跡ページ — ブランドに合わせた自社の追跡ページを提供

アーキテクチャ

┌─────────────┐     注文発送      ┌─────────────────┐
│  ECサイト    │ ───────────────→ │  バックエンド    │
│ (フロント)   │                  │  サーバー        │
└─────────────┘                  └────────┬────────┘

                               追跡登録    │    Webhook受信

                                 ┌─────────────────┐
                                 │  WhereParcel API │
                                 └────────┬────────┘

                              ステータス変更時に通知


                                 ┌─────────────────┐
                                 │  通知サービス    │
                                 │ (メール / SMS)   │
                                 └─────────────────┘

前提条件

ステップ1:追跡の自動登録

注文が発送されたタイミングで、追跡番号をWhereParcelに登録します。これにより、WhereParcelが配送状況を自動的にモニタリングしてくれます。

const axios = require('axios');

const WHEREPARCEL_API_KEY = process.env.WHEREPARCEL_API_KEY;
const WHEREPARCEL_BASE_URL = 'https://api.whereparcel.com/v2';

/**
 * 注文発送時に追跡を登録する
 */
async function registerTracking(order) {
  try {
    const response = await axios.post(
      `${WHEREPARCEL_BASE_URL}/track`,
      {
        trackingItems: [{
          carrier: order.carrierCode,       // 例: 'jp.yamato'
          trackingNumber: order.trackingNumber,
        }],
        metadata: {
          orderId: order.id,
          customerEmail: order.customerEmail,
          customerName: order.customerName
        }
      },
      {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${WHEREPARCEL_API_KEY}:${process.env.WHEREPARCEL_SECRET_KEY}`
        }
      }
    );

    console.log(`追跡登録完了: ${order.trackingNumber}`);
    return response.data;
  } catch (error) {
    console.error(`追跡登録失敗: ${error.message}`);
    throw error;
  }
}

// 注文発送処理の中で呼び出す
async function onOrderShipped(order) {
  // 1. 追跡を登録
  await registerTracking(order);

  // 2. Webhook購読を設定(ステップ2で詳述)
  await subscribeWebhook(order.trackingNumber);

  // 3. 「発送しました」メールを送信
  await sendShippingConfirmationEmail(order);
}

ステップ2:Webhookエンドポイントの構築

WhereParcelから配送ステータスの更新を受け取るエンドポイントを作成します。

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

const WEBHOOK_SECRET = process.env.WHEREPARCEL_WEBHOOK_SECRET;

/**
 * Webhook署名を検証する
 */
function verifyWebhookSignature(payload, signature) {
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

/**
 * WhereParcelからのWebhookを受け取るエンドポイント
 */
app.post('/webhooks/tracking', async (req, res) => {
  // 1. 署名を検証
  const signature = req.headers['x-webhook-signature'];
  if (!signature || !verifyWebhookSignature(req.body, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 2. 素早くレスポンスを返す(タイムアウト防止)
  res.status(200).json({ received: true });

  // 3. イベントを非同期で処理
  try {
    await processTrackingEvent(req.body);
  } catch (error) {
    console.error('Webhookイベント処理エラー:', error);
  }
});

/**
 * Webhook購読を設定する
 */
async function subscribeWebhook(trackingNumber) {
  await axios.post(
    `${WHEREPARCEL_BASE_URL}/webhooks/subscribe`,
    {
      trackingNumber,
      url: 'https://your-store.com/webhooks/tracking',
      events: [
        'tracking.in_transit',
        'tracking.out_for_delivery',
        'tracking.delivered',
        'tracking.exception'
      ]
    },
    {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${WHEREPARCEL_API_KEY}:${process.env.WHEREPARCEL_SECRET_KEY}`
      }
    }
  );
}

ステップ3:イベントに応じた処理の実装

受信したWebhookイベントの種類に応じて、適切な処理を行います。

/**
 * 追跡イベントを処理する
 */
async function processTrackingEvent(webhookPayload) {
  const { event, data } = webhookPayload;
  const { trackingNumber, status, carrier, metadata } = data;

  // 注文情報を取得
  const order = await findOrderByTrackingNumber(trackingNumber);
  if (!order) {
    console.warn(`該当する注文が見つかりません: ${trackingNumber}`);
    return;
  }

  // データベースのステータスを更新
  await updateOrderShippingStatus(order.id, status);

  // イベントの種類に応じた通知を送信
  switch (event) {
    case 'tracking.in_transit':
      await sendNotification(order, {
        subject: '商品が配送センターに到着しました',
        template: 'in-transit',
        data: {
          location: data.events[0]?.location,
          estimatedDelivery: data.estimatedDelivery
        }
      });
      break;

    case 'tracking.out_for_delivery':
      await sendNotification(order, {
        subject: '本日中にお届け予定です',
        template: 'out-for-delivery',
        data: {
          estimatedTime: data.estimatedDelivery
        }
      });
      break;

    case 'tracking.delivered':
      await sendNotification(order, {
        subject: '商品をお届けしました',
        template: 'delivered',
        data: {
          deliveredAt: data.events[0]?.timestamp,
          signedBy: data.events[0]?.description
        }
      });
      // レビュー依頼メールをスケジュール(配達3日後)
      await scheduleReviewRequest(order, 3);
      break;

    case 'tracking.exception':
      await sendNotification(order, {
        subject: '配送に問題が発生しました',
        template: 'exception',
        data: {
          reason: data.events[0]?.description
        }
      });
      // CSチームにも通知
      await alertSupportTeam(order, data);
      break;

    default:
      console.log(`未対応のイベント: ${event}`);
  }
}

ステップ4:顧客向け追跡ページの作成

顧客がリアルタイムで配送状況を確認できる追跡ページを構築します。

/**
 * 追跡ページのAPIエンドポイント
 */
app.get('/tracking/:trackingNumber', async (req, res) => {
  const { trackingNumber } = req.params;

  try {
    // WhereParcel APIで最新状況を取得
    const response = await axios.post(
      `${WHEREPARCEL_BASE_URL}/track`,
      {
        trackingItems: [{ carrier: 'auto', trackingNumber }]  // 自動検出
      },
      {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${WHEREPARCEL_API_KEY}:${process.env.WHEREPARCEL_SECRET_KEY}`
        }
      }
    );

    const trackingData = response.data.data;

    // 追跡ページをレンダリング
    res.render('tracking-page', {
      trackingNumber,
      status: trackingData.status,
      carrier: trackingData.carrier,
      estimatedDelivery: trackingData.estimatedDelivery,
      events: trackingData.events.map(event => ({
        date: new Date(event.timestamp).toLocaleString('ja-JP', {
          timeZone: 'Asia/Tokyo'
        }),
        status: translateStatus(event.status),
        location: event.location,
        description: event.description
      }))
    });
  } catch (error) {
    res.render('tracking-page', {
      error: '追跡情報を取得できませんでした。しばらくしてから再度お試しください。'
    });
  }
});

/**
 * ステータスを日本語に変換する
 */
function translateStatus(status) {
  const statusMap = {
    'picked_up': '集荷完了',
    'in_transit': '配送中',
    'out_for_delivery': '配達中',
    'delivered': '配達完了',
    'exception': '配送遅延・問題',
    'returned': '返送'
  };
  return statusMap[status] || status;
}

ステップ5:通知の設定と送信

メールやSMSで顧客に通知を送信する処理を実装します。

const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  // メール送信サービスの設定
  host: process.env.SMTP_HOST,
  port: 587,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS
  }
});

/**
 * 顧客に通知を送信する
 */
async function sendNotification(order, notification) {
  const { subject, template, data } = notification;

  // メール通知
  if (order.notificationPreferences?.email !== false) {
    await transporter.sendMail({
      from: '"Your Store" <noreply@your-store.com>',
      to: order.customerEmail,
      subject,
      html: renderEmailTemplate(template, {
        customerName: order.customerName,
        trackingNumber: order.trackingNumber,
        trackingUrl: `https://your-store.com/tracking/${order.trackingNumber}`,
        ...data
      })
    });
  }

  // SMS通知(配達中・配達完了のみ)
  const smsEvents = ['out-for-delivery', 'delivered'];
  if (smsEvents.includes(template) && order.customerPhone) {
    await sendSMS(order.customerPhone, buildSMSMessage(template, {
      trackingNumber: order.trackingNumber,
      ...data
    }));
  }

  // 通知ログを保存
  await saveNotificationLog({
    orderId: order.id,
    type: template,
    channel: 'email',
    sentAt: new Date()
  });
}

/**
 * SMSメッセージを生成する
 */
function buildSMSMessage(template, data) {
  const messages = {
    'out-for-delivery': `【Your Store】ご注文の商品(${data.trackingNumber})が本日中にお届け予定です。追跡:https://your-store.com/tracking/${data.trackingNumber}`,
    'delivered': `【Your Store】ご注文の商品(${data.trackingNumber})をお届けしました。ご確認ください。`
  };
  return messages[template];
}

ベストプラクティス

配送通知システムを運用する際に気をつけるべきポイントをまとめます。

過度な通知を避ける

すべてのステータス更新を通知するのは逆効果です。顧客が本当に知りたい重要なマイルストーンに絞りましょう:

マイルストーン通知すべきか理由
発送完了はい購入後の安心感を与える
配送センター通過いいえ細かすぎて煩わしい
配達中(本日お届け)はい受取準備ができる
配達完了はい確認のため
配送遅延・例外はい不安を先回りして解消

追跡リンクを必ず含める

通知には必ず自社の追跡ページへのリンクを含めてください。リンクがないと、顧客はCSに問い合わせることになります。

タイムゾーンを考慮する

国際配送の場合、配送イベントのタイムスタンプを顧客のタイムゾーンに変換して表示しましょう。「UTC 01:00」と表示されても、ほとんどの顧客にはわかりません。

深夜の通知を控える

顧客の現地時間で22時〜8時の間は通知を保留し、翌朝まとめて送信することを検討してください。

遅延時は先手を打つ

配送に遅延が発生した場合、顧客から問い合わせが来る前に通知を送りましょう。遅延の理由と新しい到着予定日を含めると、顧客の不安を大幅に軽減できます。

まとめ

本チュートリアルで構築したシステムは、以下の効果をもたらします:

  • WISMO問い合わせの削減 — 顧客が自分で配送状況を確認できるため、「今どこ?」という問い合わせが大幅に減少します
  • 顧客満足度の向上 — プロアクティブな通知で購入体験が向上します
  • 運用コストの削減 — CSチームの負担が軽減されます
  • リピート率の向上 — 優れた配送体験は再購入の動機になります

WhereParcelのAPIとWebhookを使えば、これらの機能をわずか数日で実装できます。APIドキュメントで詳細をご確認ください。