@repost/portal-react

React hooks for customer webhook dashboards: queries, mutations, a live delivery feed, and the iframe embed, in one package.

@repost/portal-react is the data layer of the blocks, and the way to build fully custom webhook UI. It wraps @repost/portal-js with hooks and an isolated cache: you do not need your own TanStack Query setup, and an existing one is unaffected.

pnpm add @repost/portal-react

Token endpoint

The browser never sees your API key. Add a backend endpoint that mints a portal access token for the signed-in customer, the same portal-access call that powers hosted links:

// e.g. POST /api/repost-portal-token
app.post("/api/repost-portal-token", async (req, res) => {
  const response = await fetch(
    `https://api.repost.sh/v1/customers/${req.user.customerId}/portal-access`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.REPOST_TOKEN}`, "Content-Type": "application/json" },
      body: JSON.stringify({ expiry: 3600 }),
    },
  );
  const { token } = await response.json();
  res.json({ token });
});

Tokens are short-lived and scoped to one customer in one environment. The provider calls getToken whenever it needs a fresh one: proactively before expiry, and again after any auth failure. Mint per request; never store tokens.

Provider and hooks

import { RepostPortalProvider, useLogsFeed, useReplayDelivery } from "@repost/portal-react";
 
const getToken = () =>
  fetch("/api/repost-portal-token", { method: "POST" })
    .then((res) => res.json())
    .then(({ token }) => token);
 
export function WebhookSection() {
  return (
    <RepostPortalProvider getToken={getToken}>
      <Deliveries />
    </RepostPortalProvider>
  );
}
 
function Deliveries() {
  const feed = useLogsFeed(); // websocket on top of a REST backfill — no polling
  const replay = useReplayDelivery();
 
  return feed.rows.map((row) => (
    <div key={`${row.deliveryId}:${row.attempt}`}>
      {row.type}{row.responseStatus}
      <button onClick={() => replay.mutate({ deliveryId: row.deliveryId })}>Replay</button>
    </div>
  ));
}

Query hooks return { data, isLoading, isError, error, refetch }; paginated ones add items / fetchNextPage / hasNextPage; mutations return { mutate, mutateAsync, isPending, error } and invalidate the affected queries automatically.

HooksCover
usePortalContextCustomer identity, your branding, the read-only flag.
useLogsFeed, useLogs, useLogsHistogram, useDeliveryAttemptsDelivery history, the live feed, the histogram, per-attempt detail.
useEndpoints, useEndpoint, useCreateEndpoint, useUpdateEndpoint, usePauseEndpoint, useResumeEndpoint, useDeleteEndpoint, useRevealEndpointSecret, useRotateEndpointSecretEndpoint management and signing secrets.
useDlq, useDlqCount, useDlqCountByEndpoint, useReplayDeliveryThe dead-letter queue and single-delivery replay.
usePrepareReplayJob, useConfirmReplayJob, useReplayJob, useActiveReplayJob, useCancelReplayJob, usePauseReplayJob, useResumeReplayJob, useReplayJobItemsBulk replay jobs.
useEventTypes, useSendSampleEventThe event catalog and signed test sends.
useRepostPortalThe raw portal-js client, for anything the hooks don't cover.

useLogsFeed merges realtime deliveries over a REST backfill: deduped, newest first, with a buffer cap and a dropped-row counter. There is no polling anywhere in the library.

Read-only sessions

Mint the token with readOnly: true and every read keeps working, including the live feed. Every mutation, secret reveal included, is rejected with 403 forbidden. usePortalContext exposes readOnly so your UI can hide write affordances up front.

The iframe embed

The same package exports the hosted portal embed for the zero-custom-UI path:

import { RepostPortal } from "@repost/portal-react";
 
<RepostPortal url={portalUrl} getPortalToken={getToken} />

That is the whole integration: token handling, theming, and drawers and dialogs that cover your viewport instead of being clipped to the frame. See Embed the portal for the full prop list, the CSP requirement, and troubleshooting.