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_idwhen 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_idand forward it on list, create, and publish calls.
Recommended areas / screens
| Area | Purpose |
|---|---|
| Destinations list | All 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 destination | Multi-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 attempts | Default 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 route | What it does | Spec |
|---|---|---|
…/destinations or …/integrations | Hub: list destinations; create or drill down | Listing destinations · List destinations |
…/destinations/new (or wizard) | Create destination: choose type (types; GET /destination-types in OpenAPI), then topics and config | Creating a destination |
…/destinations/:destinationId | Detail: edit config, enable/disable, topics | OpenAPI — Destinations |
…/destinations/:destinationId/activity | Activity for this destination: events, attempts, retry | Events, attempts, and retries · List events · List attempts |
…/activity (optional) | Tenant-wide activity; optional filter by destination_id | Same 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
| Goal | OpenAPI entry point | In the UI |
|---|---|---|
| Types, labels, icons, dynamic form defs | Destination types / schema — GET /destination-types | Type picker; join list rows on destination.type (the type id is type, not a separate id on the type object). |
| Topics for subscriptions | Topics — GET /topics | Checkboxes or multi-select on create/update. |
| List destinations | List destinations | Main table; show target / target_url per schema. |
| Create destination | Create destination | Body: type, topics, type-specific config / credentials per spec. |
| Get / update / delete | OpenAPI — Destinations | Detail and edit flows. |
| Tenant JWT (optional browser calls) | Tenant JWT | Short-lived token; BFF is often simpler if you need to hide capabilities. |
| Events, attempts, retry | Events, Attempts, Retry | Activity 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). Matchesdestination.typeon list rows; not namedidon the type object.label,description,icon— Display metadata;iconis often an SVG string (some older code used the namesvg). 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_urlwhen present, and subscribed topics. - Optionally fetch
GET /destination-typesin parallel and maptypestring → schema row forlabelandicon. - 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, andicon; store the chosentypestring.
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_fieldsandcredential_fieldsfor the selected type fromGET /destination-types(or a single-type endpoint if you use one—see OpenAPI). - If
remote_setup_urlis 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
- 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.
- Events — Your backend published each event (topic + payload). List events is paginated. Common filters:
destination_idfor a per-destination screen;topic, time ranges, andlimit/next/prevfor broader views. With a tenant JWT, results are limited to that tenant; with an admin key, supplytenant_id(your backend usually injects it for the signed-in account). - Attempts — One row per delivery try (status, HTTP code, timing, optional response). Tie attempts to events with
event_idanddestination_id. Tenant-wide: list attempts. Destination-scoped routes are under OpenAPI (tenant destination attempts). - Retry — Outpost retries automatically with backoff. Manual retry is
POST /retrywithevent_idanddestination_idafter 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 need | API 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
nextandprev(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_idon 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) → collectconfigand credentials per the selected type’sconfig_fieldsandcredential_fields. - [ ] When a type exposes
instructionsorremote_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/prevor 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
instructionsmarkdown are rendered safely if they contain HTML or untrusted strings. - [ ] Sensitive credential fields respect masking and “clear to edit” behavior described in the spec.