Skip to content

@hookbase/portal

The Hookbase Portal provides embeddable React components for customer-facing webhook management.

Installation

bash
npm install @hookbase/portal

Quick Start

tsx
import { HookbasePortal, EndpointList, EndpointForm } from '@hookbase/portal';
import '@hookbase/portal/styles.css';

function WebhookSettings({ portalToken }) {
  return (
    <HookbasePortal token={portalToken}>
      <EndpointForm />
      <EndpointList />
    </HookbasePortal>
  );
}

Getting a Portal Token

Generate tokens server-side using the SDK:

typescript
// Backend API endpoint
app.get('/api/webhook-portal-token', async (req, res) => {
  const customer = await getCustomerFromSession(req);

  const token = await hookbase.portalTokens.create(customer.hookbaseAppId, {
    expiresIn: 3600, // 1 hour
  });

  res.json({ token: token.token });
});

Components

HookbasePortal

The provider component that wraps all Portal components.

tsx
<HookbasePortal
  token={portalToken}
  baseUrl="https://api.hookbase.app" // Optional
  queryOptions={{
    staleTime: 60000,
    refetchOnWindowFocus: false,
  }}
>
  {children}
</HookbasePortal>

EndpointList

Displays a list of the customer's webhook endpoints.

tsx
<EndpointList
  onEndpointClick={(endpoint) => setSelected(endpoint)}
  emptyMessage="No endpoints configured"
  showStatus={true}
/>

Props:

PropTypeDescription
onEndpointClick(endpoint) => voidCalled when an endpoint is clicked
emptyMessagestringMessage when no endpoints exist
showStatusbooleanShow endpoint status indicators

EndpointForm

Form for creating new webhook endpoints.

tsx
<EndpointForm
  onSuccess={(endpoint, secret) => {
    // secret is only available on creation
    alert(`Save this secret: ${secret}`);
  }}
  onError={(error) => console.error(error)}
  defaultValues={{
    url: 'https://',
    description: '',
  }}
/>

Props:

PropTypeDescription
onSuccess(endpoint, secret?) => voidCalled on successful creation
onError(error) => voidCalled on error
defaultValuesobjectDefault form values

EndpointDetail

Shows details and management options for a single endpoint.

tsx
<EndpointDetail
  endpointId={selectedEndpoint.id}
  onDelete={() => setSelected(null)}
  showSecret={false}
/>

Props:

PropTypeDescription
endpointIdstringThe endpoint to display
onDelete() => voidCalled after deletion
showSecretbooleanAllow viewing signing secret

SubscriptionManager

Manages event type subscriptions for an endpoint.

tsx
<SubscriptionManager
  endpointId={selectedEndpoint.id}
  groupByCategory={true}
/>

Props:

PropTypeDescription
endpointIdstringThe endpoint to manage
groupByCategorybooleanGroup event types by category

EventTypeList

Displays available event types (read-only).

tsx
<EventTypeList
  groupByCategory={true}
  showDescriptions={true}
/>

MessageLog

Shows webhook delivery history.

tsx
<MessageLog
  limit={25}
  endpointId={selectedEndpoint?.id} // Optional filter
  eventType="order.*" // Optional filter
  refreshInterval={30000}
  onMessageClick={(message) => showDetail(message)}
/>

Props:

PropTypeDescription
limitnumberMessages per page
endpointIdstringFilter by endpoint
eventTypestringFilter by event type (supports wildcards)
refreshIntervalnumberAuto-refresh interval in ms
onMessageClick(message) => voidCalled when a message is clicked

MessageDetail

Shows details of a specific message delivery.

tsx
<MessageDetail
  messageId={selectedMessage.id}
  showPayload={true}
  showAttempts={true}
/>

Styling

Default Styles

Import the default styles:

tsx
import '@hookbase/portal/styles.css';

CSS Variables

Customize with CSS variables:

css
:root {
  --hookbase-primary: #3b82f6;
  --hookbase-primary-hover: #2563eb;
  --hookbase-success: #22c55e;
  --hookbase-error: #ef4444;
  --hookbase-warning: #f59e0b;
  --hookbase-border: #e5e7eb;
  --hookbase-background: #ffffff;
  --hookbase-text: #1f2937;
  --hookbase-text-muted: #6b7280;
  --hookbase-radius: 0.375rem;
}

Tailwind CSS

Components use Tailwind-compatible class names. Override with your own classes:

tsx
<EndpointList className="my-custom-list" />

Headless Mode

For complete control, use headless hooks:

tsx
import { useEndpoints, useMessages } from '@hookbase/portal';

function CustomEndpointList() {
  const { data: endpoints, isLoading } = useEndpoints();

  if (isLoading) return <MySpinner />;

  return (
    <MyList>
      {endpoints.map(ep => (
        <MyListItem key={ep.id}>{ep.url}</MyListItem>
      ))}
    </MyList>
  );
}

Hooks

useEndpoints

tsx
const {
  data: endpoints,
  isLoading,
  error,
  refetch,
} = useEndpoints();

useEndpoint

tsx
const {
  data: endpoint,
  isLoading,
} = useEndpoint(endpointId);

useCreateEndpoint

tsx
const { mutate: createEndpoint, isPending } = useCreateEndpoint();

createEndpoint({
  url: 'https://example.com/webhooks',
  description: 'Production endpoint',
});

useDeleteEndpoint

tsx
const { mutate: deleteEndpoint } = useDeleteEndpoint();

deleteEndpoint(endpointId);

useRotateSecret

tsx
const { mutate: rotateSecret } = useRotateSecret();

const newSecret = await rotateSecret(endpointId);

useEventTypes

tsx
const { data: eventTypes } = useEventTypes();

useSubscriptions

tsx
const { data: subscriptions } = useSubscriptions(endpointId);

useCreateSubscription

tsx
const { mutate: subscribe } = useCreateSubscription();

subscribe({ endpointId, eventTypeId });

useDeleteSubscription

tsx
const { mutate: unsubscribe } = useDeleteSubscription();

unsubscribe(subscriptionId);

useMessages

tsx
const { data, fetchNextPage, hasNextPage } = useMessages({
  endpointId,
  eventType: 'order.*',
  limit: 25,
});

useMessage

tsx
const { data: message } = useMessage(messageId);

Full Example

tsx
import { useState, useEffect } from 'react';
import {
  HookbasePortal,
  EndpointList,
  EndpointForm,
  SubscriptionManager,
  MessageLog,
} from '@hookbase/portal';
import '@hookbase/portal/styles.css';

export function WebhookDashboard() {
  const [portalToken, setPortalToken] = useState<string | null>(null);
  const [selectedEndpoint, setSelectedEndpoint] = useState(null);
  const [showNewEndpoint, setShowNewEndpoint] = useState(false);

  useEffect(() => {
    fetch('/api/webhook-portal-token')
      .then(res => res.json())
      .then(data => setPortalToken(data.token));
  }, []);

  if (!portalToken) {
    return <div>Loading...</div>;
  }

  return (
    <HookbasePortal token={portalToken}>
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
        {/* Endpoints Panel */}
        <div>
          <div className="flex justify-between items-center mb-4">
            <h2 className="text-xl font-semibold">Webhook Endpoints</h2>
            <button
              onClick={() => setShowNewEndpoint(true)}
              className="btn btn-primary"
            >
              Add Endpoint
            </button>
          </div>

          {showNewEndpoint && (
            <EndpointForm
              onSuccess={(endpoint, secret) => {
                alert(`Endpoint created! Save this secret: ${secret}`);
                setShowNewEndpoint(false);
                setSelectedEndpoint(endpoint);
              }}
              onCancel={() => setShowNewEndpoint(false)}
            />
          )}

          <EndpointList
            onEndpointClick={setSelectedEndpoint}
            selectedId={selectedEndpoint?.id}
          />
        </div>

        {/* Subscriptions Panel */}
        <div>
          {selectedEndpoint ? (
            <>
              <h2 className="text-xl font-semibold mb-4">
                Subscriptions for {selectedEndpoint.url}
              </h2>
              <SubscriptionManager
                endpointId={selectedEndpoint.id}
                groupByCategory={true}
              />
            </>
          ) : (
            <p className="text-gray-500">
              Select an endpoint to manage subscriptions
            </p>
          )}
        </div>
      </div>

      {/* Message Log */}
      <div className="mt-8">
        <h2 className="text-xl font-semibold mb-4">Recent Deliveries</h2>
        <MessageLog
          limit={25}
          endpointId={selectedEndpoint?.id}
          refreshInterval={30000}
        />
      </div>
    </HookbasePortal>
  );
}

Browser Support

  • Chrome 80+
  • Firefox 75+
  • Safari 13+
  • Edge 80+

Requires React 18 or later.

Released under the MIT License.