← ユースケース

外部サービスの Webhook をユーザーが開いているページに届ける

支払い確定、配送状況の更新、ビルド完了:Stripe、配送業者、GitHub の Webhook を、ユーザーが見ているページへ再読み込みなしで届けます。

お客様が Stripe Checkout で支払いを済ませて「ありがとうございました」ページに戻っても、支払いの確定を知らせる Webhook は数秒遅れて届くことがあります。ページには「お支払いを確認しています…」と表示され、お客様は再読み込みしたり、もう一度支払ってしまったりします。配送業者、CI サービス、電子署名ツール、マーケットプレイスも同じで、知らせが届くのはサーバーであって、ユーザーが見ているページではありません。Hibira なら、Webhook のハンドラーが受け取った瞬間に、その更新をページへ転送します。

ユーザーが得るもの

  • 見ているページにそのまま「お支払いを受け付けました」と表示され、再読み込みは不要。
  • 配送、ビルド、署名の進捗がひとりでに表示される。
  • 一度目が失敗したと思い込んだ二重払いがなくなる。

使う機能

  • 注文した本人だけが参加できるプライベートチャンネル(private-order-42)。認可エンドポイントで確認します。
  • 事業者の署名を検証して変更を保存する、その同じ Webhook ハンドラーで呼ぶ trigger。
  • pusher:subscription_succeeded。現在の状態を一度読み込むので、ページを開く前に届いた Webhook も取りこぼしません。
ブラウザーあなたのサーバーStripeHibiraCheckout で支払う/orders/42 へリダイレクト「お支払いを確認しています…」Webhookcheckout.session.completedtrigger("status")private-order-42status: paid領収書を表示
事業者はサーバーに知らせ、サーバーは Hibira 経由でページに知らせる

ステップ 1:保存してから Webhook を転送する

署名を検証し、処理済みのイベントは無視し(事業者は再送します)、新しい状態を保存してから配信します:

app.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
  if (await db.webhookEvents.seen(event.id)) return res.sendStatus(200);
  if (event.type === 'checkout.session.completed') {
    const orderId = event.data.object.metadata.orderId;
    await db.orders.update(orderId, { status: 'paid' });
    await pusher.trigger(`private-order-${orderId}`, 'status', { status: 'paid' });
  }
  res.sendStatus(200);
});

ステップ 2:本人だけが受信できるようにする

誰が購読できるかはチャンネル認可エンドポイントで決めます:

app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  const id = req.body.channel_name.match(/^private-order-(.+)$/)?.[1];
  if (!id || !(await db.orders.ownedBy(id, req.user.id))) return res.sendStatus(403);
  res.json(pusher.authorizeChannel(req.body.socket_id, req.body.channel_name));
});

ステップ 3:ページで待ち受け、状態を一度読む

先に購読し、準備ができたら現在の状態を一度読み込みます。こうすれば Webhook がページより先に届いても後に届いても問題ありません:

const channel = pusher.subscribe('private-order-42');
channel.bind('status', ({ status }) => show(status));
channel.bind('pusher:subscription_succeeded', async () => {
  const order = await fetch('/api/orders/42').then((r) => r.json());
  show(order.status);
});

ほかの Webhook も同じ方法で

ヒント

  • 事業者にはすぐ 200 を返します。時間のかかる処理は応答後かジョブで行います。長い処理の進捗をご覧ください。
  • ページに必要なもの(状態と ID)だけを転送し、事業者の元のペイロードは転送しません。個人情報が含まれることがあります。
  • ユーザーがページを閉じている場合は、プッシュ通知ガイドでプッシュ通知を送ります。
  • チーム全体のダッシュボードなら、注文ごとではなくチームごとに 1 チャンネルにできます。