---
title: "@repost/portal-react"
description: "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](/docs/ui/blocks), and the way to build fully custom webhook UI. It wraps [`@repost/portal-js`](/docs/ui/portal-js) with hooks and an isolated cache: you do not need your own TanStack Query setup, and an existing one is unaffected.

```bash
npm install @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](/docs/send/portal) that powers hosted links:

```ts
// 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

```tsx
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.

| Hooks | Cover |
|-------|-------|
| `usePortalContext` | Customer identity, your branding, the read-only flag. |
| `useLogsFeed`, `useLogs`, `useLogsHistogram`, `useDeliveryAttempts` | Delivery history, the live feed, the histogram, per-attempt detail. |
| `useEndpoints`, `useEndpoint`, `useCreateEndpoint`, `useUpdateEndpoint`, `usePauseEndpoint`, `useResumeEndpoint`, `useDeleteEndpoint`, `useRevealEndpointSecret`, `useRotateEndpointSecret` | Endpoint management and signing secrets. |
| `useDlq`, `useDlqCount`, `useDlqCountByEndpoint`, `useReplayDelivery` | The dead-letter queue and single-delivery replay. |
| `usePrepareReplayJob`, `useConfirmReplayJob`, `useReplayJob`, `useActiveReplayJob`, `useCancelReplayJob`, `usePauseReplayJob`, `useResumeReplayJob`, `useReplayJobItems` | Bulk replay jobs. |
| `useEventTypes`, `useSendSampleEvent` | The event catalog and signed test sends. |
| `useRepostPortal` | The raw [`portal-js` client](/docs/ui/portal-js), 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](/docs/send/portal) embed for the zero-custom-UI path:

```tsx
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](/docs/send/portal-embed) for the full prop list, the CSP requirement, and
troubleshooting.
