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

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

Source: https://hibira.io/ja/use-cases/webhooks-to-browser

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

## ユーザーが得るもの

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

## 使う機能

- 注文した本人だけが参加できる**プライベートチャンネル**（`private-order-42`）。認可エンドポイントで確認します。
- 事業者の署名を検証して変更を保存する、その同じ Webhook ハンドラーで呼ぶ **`trigger`**。
- **`pusher:subscription_succeeded`**。現在の状態を一度読み込むので、ページを開く前に届いた Webhook も取りこぼしません。

```text
sequence: 事業者はサーバーに知らせ、サーバーは Hibira 経由でページに知らせる
ブラウザー | あなたのサーバー | Stripe | *Hibira
1 > 3: Checkout で支払う
3 >> 1: /orders/42 へリダイレクト | 「お支払いを確認しています…」
3 > 2: Webhook | checkout.session.completed
2 > 4: trigger("status") | private-order-42
4 > 1: status: paid | 領収書を表示
```

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

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

```js
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：本人だけが受信できるようにする

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

```js
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 がページより先に届いても後に届いても問題ありません：

```js
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 も同じ方法で

| 送り元 | Webhook | 開いているページに表示するもの |
| --- | --- | --- |
| Stripe、PayPal、Adyen | 支払いの成功・失敗 | 領収書、または再試行ボタン |
| 配送業者 | 追跡情報の更新 | 注文ページの新しいステップ |
| GitHub、GitLab、Vercel | ビルド・デプロイ完了 | ステータスバッジとプレビューリンク |
| 電子署名ツール | 署名完了 | 「双方が署名しました」 |
| Shopify、マーケットプレイス | 注文作成・支払い | 販売者ダッシュボードに新しい行 |

## ヒント

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