Building Your Own UI

While Outpost offers a Tenant User Portal, you may want to build your own UI so your customers can manage their destinations and view delivery activity.

This page is for teams building that destination management experience — usually product engineers and anyone designing settings, integrations, or support tooling around webhooks and other destination types. It is framework-agnostic, covering screens, flows, and how they map to Outpost. If you use an AI coding assistant you can provide it with workflow-specific instructions; this guide stays focused on what your customers should see and what your backend should enforce.

The portal uses the same Outpost API you can call from your product. Its source is a useful reference (internal/portal, React); you are not required to match its stack.

For paths, query parameters, request and response JSON, status codes, and authentication, use the OpenAPI specification as the authoritative contract. If anything here disagrees with OpenAPI, trust the spec.

Prefer official SDKs on the server where Hookdeck provides them for your backend language—see the SDK overview and the curl, TypeScript, Python, or Go quickstart in this documentation for runnable examples. The SDKs wrap the same API: less boilerplate, typed clients, and fewer raw HTTP mistakes. Use OpenAPI as the contract for wire JSON (especially when your browser or BFF returns JSON that should match the HTTP API), for generated clients, or when you integrate from a stack without a first-party SDK.

Working from OpenAPI

Map each surface in your product to named operations in the spec (list destinations, create destination, list events, and so on). Use the published schemas for request bodies and list rows, and implement those operations with the official SDK on your backend when available.

Destination type labels, icons, and dynamic form fields come from GET /destination-types—specifically config_fields and credential_fields (see Destination type metadata and dynamic config). That response is the source for field keys and types, not guesses from older examples. Each field object includes a key: the property name inside the destination’s config or credentials object (for example url for a webhook). This is documented on DestinationSchemaField in OpenAPI.

Whether the browser uses a tenant JWT or talks only to your API, the operations are the ones in OpenAPI; see Authentication for how credentials and tenant_id are applied.

The portal shows full UI code for complex forms; this page avoids long framework-specific snippets so the spec stays the single place for shapes and validation.

UI structure and flow

The tenant portal illustrates how screens map to tenant → destinations → topics → delivery target. Following that shape helps your customers understand subscriptions and targets instead of a single anonymous “webhook URL.”

Tenant context

  • Everything below applies to one tenant at a time: the signed-in account in your SaaS (your customer). Use that account’s tenant_id when listing or creating destinations and when publishing from your backend.
  • With a tenant JWT, the token is scoped to that tenant. If you proxy through your API, resolve the signed-in account to tenant_id and forward it on list, create, and publish calls.

Recommended areas / screens

AreaPurpose
Destinations listAll destinations for the current tenant (type, human-readable target such as webhook URL, queue name, or Hookdeck label, plus subscribed topics). Entry point to edit, disable, or remove.
Create destinationMulti-step flow: (1) choose destination type, (2) select topics from your Outpost project configuration, (3) fill type-specific config from the type schema. Optional: instructions or remote setup URL from the schema.
Events and delivery attemptsDefault pattern: open activity from a destination (events, then attempts, then retry in that context). Optional: a tenant-wide activity view with a destination filter for support or power users. See Default information architecture and Events, attempts, and retries.

Default information architecture (multi-destination products)

When a tenant can have many destinations—of any destination type your project enables—the primary path is destination → activity: people ask “what was delivered to this subscription?” rather than seeing all traffic in one undifferentiated list. The same API applies for webhooks, queues, and other types; only create/edit forms differ, driven by destination type metadata and dynamic config.

For list events and list attempts, reuse the same endpoints everywhere: vary query parameters (for example destination_id, cursors) rather than inventing parallel client-side contracts. Pagination and auth details are defined in OpenAPI; Events, attempts, and retries below summarizes how those endpoints support common UI needs.

Example routes (rename to fit your product—integrations, event destinations, webhooks, etc.):

Example routeWhat it doesSpec
…/destinations or …/integrationsHub: list destinations; create or drill downListing destinations · List destinations
…/destinations/new (or wizard)Create destination: choose type (types; GET /destination-types in OpenAPI), then topics and configCreating a destination
…/destinations/:destinationIdDetail: edit config, enable/disable, topicsOpenAPI — Destinations
…/destinations/:destinationId/activityActivity for this destination: events, attempts, retryEvents, attempts, and retries · List events · List attempts
…/activity (optional)Tenant-wide activity; optional filter by destination_idSame list-events operation with different query params (OpenAPI)

For the conceptual model, see Outpost Concepts, especially “How this fits your product.”

OpenAPI: core operations for a tenant dashboard

GoalOpenAPI entry pointIn the UI
Types, labels, icons, dynamic form defsDestination types / schemaGET /destination-typesType picker; join list rows on destination.type (the type id is type, not a separate id on the type object).
Topics for subscriptionsTopicsGET /topicsCheckboxes or multi-select on create/update.
List destinationsList destinationsMain table; show target / target_url per schema.
Create destinationCreate destinationBody: type, topics, type-specific config / credentials per spec.
Get / update / deleteOpenAPI — DestinationsDetail and edit flows.
Tenant JWT (optional browser calls)Tenant JWTShort-lived token; BFF is often simpler if you need to hide capabilities.
Events, attempts, retryEvents, Attempts, RetryActivity and recovery; see below.

Authentication

You can issue a tenant JWT for client-side calls to Outpost, or proxy requests through your own API. With a proxy, attach your platform’s Outpost API key on the server and scope each call to the authenticated tenant (for example via tenant_id on admin-key routes).

Proxying is useful when you want to restrict which Outpost features are exposed or to keep the admin key off the client entirely.

Browser, your API, and Outpost (BFF pattern)

In a typical backend-for-frontend arrangement, the customer’s browser calls your product API only. Your servers call Outpost with the platform API key and the correct tenant_id for the signed-in account. Teams refer to this as a BFF, an Outpost proxy, or a server-side integration layer—the pattern is the same.

The alternative is for the browser to call Outpost directly using a short-lived tenant JWT (Generating a JWT Token below). Many products prefer a proxy so the admin key never ships to the client and so they can limit which Outpost capabilities the UI may invoke.

API base URL (managed and self-hosted)

Use one configurable base URL for Outpost (no trailing slash), for example API_URL or OUTPOST_API_BASE_URL. Paths in this guide match OpenAPI (/tenants/..., /topics, /destination-types, …).

  • Managed Hookdeck Outpost: use the base URL from your project (see the curl quickstart).
  • Self-hosted: use your deployment’s public origin plus any path prefix (often /api/v1). Local development should still read host and port from configuration or environment so the same code works in staging and production.

In your product, treat the base URL like any other external service: load it from config or env, not from literals baked into client bundles.

Generating a JWT Token (Optional)

See the Tenant JWT Token API.

export OUTPOST_API_BASE_URL="https://api.outpost.hookdeck.com/2025-07-01"   # or your self-hosted root, e.g. …/api/v1
TENANT_ID="<TENANT_ID>"

curl --request GET "$OUTPOST_API_BASE_URL/tenants/$TENANT_ID/token" \
  --header "Authorization: Bearer <ADMIN_API_KEY>"

Destination type metadata and dynamic config

GET /destination-types returns everything needed to render type pickers and config forms. See the Destination Types Schema API.

Each entry typically includes (confirm names and optionality in OpenAPI):

  • type — Stable identifier (e.g. webhook). Matches destination.type on list rows; not named id on the type object.
  • label, description, icon — Display metadata; icon is often an SVG string (some older code used the name svg). Sanitize if you render inline HTML.
  • config_fields, credential_fields — Field definitions for the config step (snake_case in JSON). Include every field from both arrays on create and edit.
  • instructions — Markdown for complex setup (for example cloud resources).
  • remote_setup_url — Optional external setup flow before or instead of inline fields.

Dynamic field shape (for forms)

Field objects are fully described in OpenAPI (DestinationSchemaField), including key (where to place the value in config / credentials on create/update). Each field has label, type (text vs checkbox vs select vs key-value map), required, optional description, validation (minlength, maxlength, pattern), default, disabled, and sensitive (password-style; values may be masked after create—clear to edit). On submit, map each value to the key Outpost expects inside config / credentials, regardless of how property names were transformed earlier in your stack—see Wire JSON, SDK responses, and your UI.

Reference: DestinationConfigFields.tsx maps schema fields to inputs.

Wire JSON, SDK responses, and your UI

This section matters whether you use an official SDK on the server (recommended when available) or raw HTTP: the HTTP API always follows OpenAPI, while SDKs present language-native types to your backend code.

HTTP responses from Outpost on the wire use JSON property names that match OpenAPI—typically snake_case (for example config_fields, credential_fields, and remote_setup_url on GET /destination-types).

Official SDKs deserialize into language-native structures; names often differ from the wire format (for example TypeScript may expose camelCase such as configFields and credentialFields). Mutations use each SDK’s documented request types, which may not mirror OpenAPI field names literally.

When a browser loads destination-type metadata via your backend, it receives whatever JSON your server returns. Options include forwarding the raw Outpost response body (so the client matches OpenAPI) or translating once on the server and treating that as your product’s contract. In all cases, create and update bodies must still place each value under the schema field’s key inside config and credentials as defined in OpenAPI.

Shape mismatches between layers often appear as missing dynamic fields or create errors referencing absent config.* keys (for example config.url for webhooks). Comparing the actual JSON your UI receives with the property names your rendering code expects (config_fields versus configFields, and similar) usually isolates the problem.

Remote setup URL

When remote_setup_url is present, you can link users through an external setup flow (for example Hookdeck-managed configuration) instead of only inline fields.

Instructions

Render instructions as markdown when the destination type needs context beyond simple fields.

Listing configured destinations

Use the List Destinations API. OpenAPI describes variants for admin API key (tenant in path or query) versus tenant JWT (tenant inferred from the token); choose the operations that match how you authenticate.

  • Call list and render type, target, target_url when present, and subscribed topics.
  • Optionally fetch GET /destination-types in parallel and map type string → schema row for label and icon.
  • Link each row to destination detail and destination-scoped activity (Default information architecture).

Reference: DestinationList.tsx

Creating a destination

The product flow is three steps; the API is typically one create destination request once you have type, topics, and config (plus credentials if required). OpenAPI defines the body.

Step 1 — Choose destination type

  • Data: GET /destination-types (schemas).
  • Show each type’s label, description, and icon; store the chosen type string.

Reference: CreateDestination.tsx

Step 2 — Select topics

  • Data: GET /topics (list topics).
  • Collect topic strings, or * for all topics, as allowed by the create schema.

Reference: TopicPicker.tsx

Step 3 — Configure the destination

  • Read config_fields and credential_fields for the selected type from GET /destination-types (or a single-type endpoint if you use one—see OpenAPI).
  • If remote_setup_url is set, consider that flow first.
  • Otherwise render fields per Dynamic field shape and submit via Create destination.

Reference: DestinationConfigFields.tsx

Events, attempts, and retries

This section connects what your customers see (what was delivered, what failed, how to retry) to the API. Request and response shapes live in OpenAPI; the portal shows one full implementation.

How the pieces fit

  1. Destinations list — Each row is a subscription. By default, link into destination-scoped activity (Default information architecture). An optional tenant-wide activity route should still call the same list endpoints with different query parameters, not a separate unofficial API contract.
  2. Events — Your backend published each event (topic + payload). List events is paginated. Common filters: destination_id for a per-destination screen; topic, time ranges, and limit / next / prev for broader views. With a tenant JWT, results are limited to that tenant; with an admin key, supply tenant_id (your backend usually injects it for the signed-in account).
  3. Attempts — One row per delivery try (status, HTTP code, timing, optional response). Tie attempts to events with event_id and destination_id. Tenant-wide: list attempts. Destination-scoped routes are under OpenAPI (tenant destination attempts).
  4. Retry — Outpost retries automatically with backoff. Manual retry is POST /retry with event_id and destination_id after the customer fixes their endpoint. The destination must be enabled and subscribed to the event’s topic.

What to expose in your dashboard UI

User needAPI direction
“What was delivered here?” (this destination)List events with destination_id, then list attempts for the chosen event_id (and destination as needed)—same idea for webhooks, queues, and other types.
“Why did it fail?”Surface attempt status, code, and response when present; link to your docs on URLs, auth, or timeouts.
“Send it again”Retry on failed attempts → POST /retry; handle 202 vs errors such as disabled destination.

Implementation notes

  • Event and attempt lists use cursor pagination; pass through next and prev (or “load more”) for busy tenants.
  • If the browser never holds the admin key, proxy these calls through your backend with the platform key and the correct tenant_id, same as destination CRUD.
  • Reference: Events.tsx for destination-scoped activity layout.

Implementation checklists

Use these lists before launch, in design or code review, or when comparing your tenant experience to the patterns above. They do not replace OpenAPI, security review, or testing against your deployment.

For customer-facing destination and delivery UI, work through Planning and contract, Destinations experience, and Activity, attempts, and retries at minimum. Skip rows that clearly do not apply (for example, if you only expose destinations through your own API and have no in-app activity screens—document how customers verify delivery instead).

Planning and contract

  • [ ] Every call is scoped to the correct tenant (tenant_id on admin-key routes, or tenant inferred from JWT).
  • [ ] Outpost base URL comes from configuration or environment for dev, staging, and production (not a single hardcoded host in app code).
  • [ ] Server-side Outpost calls use an official SDK when Hookdeck ships one for your language; raw HTTP or generated OpenAPI clients are fine when they fit better.
  • [ ] You chose an auth approach (browser JWT, server-side proxy/BFF, or mix) and use the matching OpenAPI operations and headers consistently.
  • [ ] Dynamic destination UI (labels, icons, form fields) is driven by GET /destination-types, not copied field lists from examples.

Destinations experience

  • [ ] List view shows type, human-readable target, and subscribed topics; each row reaches detail edit and destination-scoped activity.
  • [ ] Create flow covers: pick type → select topics (GET /topics) → collect config and credentials per the selected type’s config_fields and credential_fields.
  • [ ] When a type exposes instructions or remote_setup_url, the UI surfaces them (markdown / external flow) so customers are not blocked on opaque fields.
  • [ ] Detail supports lifecycle your product needs: view, update, delete, enable/disable—per OpenAPI and your product policy.

Activity, attempts, and retries

  • [ ] Default path is destination → events → attempts; optional tenant-wide activity still uses the same list endpoints with different query parameters.
  • [ ] Cursor pagination is implemented for busy tenants (next / prev or equivalent “load more”).
  • [ ] Failed deliveries show enough context (status, HTTP code, response when present) for customers to fix their side.
  • [ ] Manual retry is available where appropriate; errors such as disabled destination are handled with a clear message.

Content from the API

  • [ ] Inline icons or instructions markdown are rendered safely if they contain HTML or untrusted strings.
  • [ ] Sensitive credential fields respect masking and “clear to edit” behavior described in the spec.