Embedding the portal
The embedded portal is an iframe your customers use to manage their own destinations without leaving your product. They see only their destinations, they can create and edit per their permissions, and your application code never holds destination secrets. The portal renders against a short-lived session token your backend mints; everything else is configuration.
What the embed gives you
End-customers can browse destinations, create new ones from the supported types, edit configuration, rotate secrets, view delivery logs scoped to their own data, and launch replays. The portal is theme-able so it matches your product's visual language.
Your code does not handle destination configuration at all, the portal is a closed loop between the customer and the Pushrail API. That keeps secrets out of your application's logs and out of your support team's view.
Install
Two packages exist; pick whichever fits your front end.
pnpm add @pushrail/embed-react
pnpm add @pushrail/embed-js
The React package exports a PushrailPortal component. The vanilla package exposes a Pushrail.mount({ ... }) function and is also auto-attached to window.Pushrail when loaded via a <script> tag.
Mint a session
The portal authenticates with a short-lived embed session token. Your backend mints one per portal page load using a manage-scoped API key. See the embed sessions endpoint for the full request shape.
// In your backend, behind your own auth
const resp = await fetch("https://api.pushrail.io/embed/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PUSHRAIL_ADMIN_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
customerExternalId: req.user.customerId,
permissions: ["destinations.read", "destinations.write"],
ttlSeconds: 600,
}),
});
const { token } = await resp.json();
The token is bound to the customerExternalId you pass in, the portal can only see and modify that customer's destinations, enforced server-side. Sessions are short-lived (default 10 minutes) and rotated on every portal load.
createSession options
| Option | Type | Description |
|---|---|---|
| customerExternalId | string | Identifies the customer in your system. Pushrail creates or updates the corresponding customer account automatically. |
| customerName | string | Display name for the customer. Stored and updated on every session create. |
| permissions | string[] | One or more embed permission strings (e.g. destinations.read, destinations.write, routing.read). The portal only renders the controls your permissions allow. |
| ttlSeconds | number | Token lifetime in seconds. Min 60, max 86400. Defaults to 3600. |
| metadata | object | Optional free-form key/value attached to the session for audit purposes. |
| allowedTypes | string[] | Restricts which destination types the session may create. When set, the portal type picker shows only the listed types. If exactly one type is listed, the portal skips the type-picker step and goes straight to configuration. When omitted or empty, all types the customer's plan permits are available. |
Example — restrict to webhook destinations only:
body: JSON.stringify({
customerExternalId: req.user.customerId,
customerName: req.user.organizationName,
permissions: ["destinations.read", "destinations.write", "routing.read", "routing.write"],
ttlSeconds: 600,
allowedTypes: ["WEBHOOK"],
})
Mount the portal
Hand the token to the client and mount the portal in a container of your choosing.
React:
import { PushrailPortal } from "@pushrail/embed-react";
export function DestinationsPage({ sessionToken }: { sessionToken: string }) {
return (
<PushrailPortal
session={sessionToken}
theme={{
colors: { primary: "#f97316" },
mode: "dark",
}}
onEvent={(ev) => {
if (ev.type === "ready") console.log("portal ready");
if (ev.type === "error") console.error(ev.message);
}}
style={{ height: 720 }}
/>
);
}
Vanilla JS:
import { Pushrail } from "@pushrail/embed-js";
const instance = Pushrail.mount({
container: "#pushrail-portal",
token: sessionToken,
theme: { colors: { primary: "#f97316" }, mode: "dark" },
onReady: () => console.log("portal ready"),
onError: (err) => console.error(err),
});
// Later, on token refresh:
instance.updateToken(newToken);
// On unmount:
instance.unmount();
The iframe auto-resizes its height to fit content unless you set an explicit style.height. Pass theme tokens to match your product's colors, border radius, and typography.
PushrailPortal props
| Prop | Type | Default | Description |
|---|---|---|---|
| session | string | required | The embed session token minted by your backend. |
| theme | PushrailThemeTokens | — | Theme overrides (colors, border radius, font family, light/dark mode). |
| initialRoute | string | — | Open the portal at a specific sub-path on mount. |
| section | "destinations" \| "deliveries" \| "events" | — | Pin the portal to a single top-level section. Hides the tab bar and blocks navigation between sections. |
| chrome | "full" \| "none" | "full" | Controls portal-level chrome. "none" hides the tab bar regardless of permissions. |
| maxWidth | string \| number | "1024px" | Inner content max-width inside the iframe. Accepts a CSS length string or a number (treated as px). |
| header | "full" \| "none" | "full" | Controls the portal's own page header. Set to "none" when your host page already provides the heading and you want the portal to start with the content area directly. |
| suggestedEventTypes | string[] | — | Pre-populates the event-type routing toggles in the quick-add flow. When provided, the destination-create form pre-checks the listed event types as routing suggestions. The customer can adjust them, but at least one must remain checked before saving. Useful when you already know which events should route to the destination your customer is about to create. |
| quickAddTitle | string | — | Heading shown inside the focused configurator panel when the session is scoped to a single destination type. Lets you tailor the prompt to your product's context (e.g. "Set up your alert webhook"). |
| quickAddSubtitle | string | — | Supporting text shown below the heading in the focused configurator panel. Use it to explain what the destination will receive or why the customer is setting it up. |
| onEvent | (event: PushrailEmbedEvent) => void | — | Callback for portal events: ready, navigate, resize, error. |
| baseUrl | string | — | Override the portal base URL (useful for self-hosted or staging deployments). |
| className | string | — | CSS class applied to the outer iframe wrapper. |
| style | CSSProperties | — | Inline styles for the outer iframe wrapper. |
| title | string | — | title attribute on the iframe element (accessibility). |
| sandbox | string | — | Override the sandbox attribute on the iframe. Use with caution. |
Scoped quick-add
When you pass allowedTypes in createSession with exactly one destination type, the portal enters scoped mode. In scoped mode the portal shows a focused, form-first configurator rather than the full destination list:
- The Configure and Delivery Logs tabs are shown; the full tab bar is reduced to only those two sections.
- The destination list is bypassed. If no destination has been created yet, the configuration form opens immediately on load. If one already exists, the Configure tab shows it directly.
- The type-picker step is skipped entirely; the session cannot create any other destination type.
- The form provides a Save webhook button and a Send test button in one step, so the customer can verify connectivity without navigating to a separate test flow.
- If you also pass
suggestedEventTypesto thePushrailPortalcomponent, those event types are pre-checked in the routing step that follows destination creation. Your customer can adjust the selection, but at least one must remain checked. - Use
quickAddTitleandquickAddSubtitleto replace the default heading and supporting text inside the configurator panel with copy tailored to your product's context.
This is the recommended pattern when you embed the portal as part of a focused setup flow, for example guiding a customer through setting up a single webhook destination for failure notifications:
// Backend: mint a webhook-only session
const { token } = await mintSession({
customerExternalId: customer.id,
customerName: customer.name,
permissions: ["destinations.read", "destinations.write", "routing.read", "routing.write"],
allowedTypes: ["WEBHOOK"],
});
// Frontend: focused configurator with custom copy
<PushrailPortal
session={token}
header="none"
suggestedEventTypes={["destination.unhealthy", "destination.degraded", "destination.recovered"]}
quickAddTitle="Set up your alert webhook"
quickAddSubtitle="We'll send a request to this URL whenever a destination becomes unhealthy or recovers."
style={{ minHeight: 600 }}
/>
Read events / errors
The portal posts structured events back to the host page: ready when the iframe finishes loading, navigate when the user moves between portal screens, resize when content height changes, and error for any unrecoverable problem. The React onEvent and the vanilla onReady / onError / onDestinationCreated / onDestinationUpdated callbacks give you typed access to each.
Common use cases: kick a re-fetch in your app when destination.created fires (so your own UI reflects the new destination), or call your own analytics on ready to measure portal load time.
The cross-frame protocol is documented in @pushrail/embed-js's source, origin checks are enforced on both sides, so a malicious page cannot post fake events into your handler.