← Use cases

Show third-party webhooks on the user's open page

Payment confirmed, shipment moved, build finished: forward Stripe, carrier or GitHub webhooks to the page the user is looking at, with no refresh.

A customer pays on Stripe Checkout and comes back to your "Thank you" page, but the payment is confirmed by a webhook that may arrive a few seconds later. The page says "Confirming your payment…" and they refresh, or worse, pay again. Shipping carriers, CI services, e-signature tools and marketplaces work the same way: the news reaches your server, not the page the user is looking at. With Hibira, your webhook handler forwards the update to that page the moment it arrives.

What your users get

  • "Payment received" on the page they are already looking at, without a refresh.
  • Tracking, build and signature updates that appear by themselves.
  • No double payments from people who thought the first one failed.

Built with

  • Private channels (private-order-42) that only the order's owner may join, checked by your authorization endpoint.
  • trigger from the same webhook handler that verifies the provider's signature and saves the change.
  • pusher:subscription_succeeded, to read the current status once, so a webhook that arrived before the page opened is not missed.
BrowserYour serverStripeHibirapays on Checkoutredirect to /orders/42"Confirming your payment…"webhookcheckout.session.completedtrigger("status")private-order-42status: paidshows the receipt
The provider tells your server; your server tells the page through Hibira

Step 1: forward the webhook after you save it

Verify the signature, ignore events you have already handled (providers retry), save the new state, then publish:

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);
});

Step 2: let only the owner listen

Your channel authorization endpoint decides who may subscribe:

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));
});

Step 3: listen on the page and read the status once

Subscribe first, then read the current status when the subscription is ready, so it does not matter whether the webhook comes before or after the page loads:

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);
});

The same pattern for other webhooks

Tips

  • Answer the provider with 200 quickly. Do slow work after you respond or in a job, as in progress for long jobs.
  • Forward only what the page needs (a status and an ID), never the provider's raw payload: it can hold personal data.
  • If the user has closed the page, send a push notification with the push notifications guide.
  • For a whole team's dashboard, publish to one channel per team instead of per order.