npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yuno-payments/dashboard-embed-sdk

v2.1.0

Published

Lightweight SDK for embedding the Yuno Dashboard via iframe

Downloads

1,696

Readme

@yuno-payments/dashboard-embed-sdk

Lightweight SDK for embedding the Yuno Dashboard via iframe. Zero dependencies — uses only DOM APIs.

Installation

npm install @yuno-payments/dashboard-embed-sdk

Quick Start

import { initDashboard } from "@yuno-payments/dashboard-embed-sdk";

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token: "your-jwt-token",
  theme: {
    // Simple: flat tokens apply to both light mode and dark mode;
    tokens: { primary: "#134AC3", surface: "#FFFFFF" },
    // Per-mode: full control over both light and dark colors
    // tokens: {
    //   light: { primary: "#134AC3", surface: "#FFFFFF" },
    //   dark: { primary: "#5B7BFF", surface: "#0A0A0A" },
    // },
    typography: {
      fontFamily: "'Inter', sans-serif",
      fontUrl: "https://fonts.googleapis.com/css2?family=Inter",
    },
    mode: "light",
    styles: ".yuno-card { border-radius: 8px; }",
  },
  lang: "en",
  path: "/connections",
  onReady: () => console.log("Dashboard is ready"),
});

API

Singleton helpers

The recommended way to manage the SDK instance:

import {
  initDashboard,
  getDashboard,
  destroyDashboard,
} from "@yuno-payments/dashboard-embed-sdk";

// Create the single dashboard instance
const sdk = initDashboard(config);

// Retrieve the current instance from anywhere
const sdk = getDashboard();

// Destroy the instance and clean up
destroyDashboard();

The SDK runs a single instance. Call initDashboard() once, then drive that instance — navigate() to change pages, setTheme()/setLang()/setToken() for runtime updates. Calling initDashboard() again while an instance already exists throws: re-initializing reloads the iframe and, with syncUrl on, the new path is overridden by the previously synced route. To genuinely tear down and start over, call destroyDashboard() first.

// ❌ Don't re-init to change pages — this throws on the 2nd call:
initDashboard({ ...config, path: "/connections" });
initDashboard({ ...config, path: "/checkout-builder" });

// ✅ Init once, then navigate:
const sdk = initDashboard({ ...config, path: "/connections" });
sdk.navigate("/checkout-builder");

// ✅ Or explicitly tear down first if you really need a fresh instance:
destroyDashboard();
initDashboard({ ...config, path: "/checkout-builder" });

initDashboard(config)

| Option | Type | Required | Description | |---|---|---|---| | baseUrl | string | Yes | Dashboard base URL | | container | HTMLElement | Yes | Element to mount the iframe | | token | string | No | JWT auth token — sent via PostMessage after the iframe is ready | | theme | DashboardTheme | No | Initial theme configuration | | lang | string | No | Language code (default: "en") | | path | string | No | Initial navigation path (default: "/"). Any query string you pass is stripped — only the SDK sets the iframe query params (embed/theme/lang/test) | | testMode | boolean | No | When set, the dashboard mounts in test/sandbox mode (true) or live mode (false). Omit to inherit the dashboard's own default. See Test mode | | onReady | () => void | No | Callback invoked when the dashboard is fully loaded and authenticated | | onSessionExpired | () => void | No | Callback invoked when the embedded session has expired. The host should re-authenticate and call setToken(newToken) to resume. | | onNavigationChange | (event: NavigationChangedEvent) => void | No | Fires on every in-app navigation inside the dashboard, delivering the new route via event.path. A cross-cutting notification (like onReady), not a domain event. Optional — with syncUrl on (default) the SDK already keeps the host URL in sync. See Navigation. | | events | DashboardEvents | No | Notifications the dashboard sends the host, grouped by domain and action — e.g. events.connections.create, events.connections.delete, events.checkout.published, events.checkout.publishAvailabilityChanged, events.customCheckout.* (instance lifecycle + selection). Pure callbacks: providing a handler does not change what the dashboard does. To make the dashboard step aside (e.g. skip its connection success screen) or hand you the save decision, use ui. See Embed events. | | loading | HTMLElement | No | Custom loading overlay element. If omitted, a default spinner is shown | | autoHeight | boolean | No | When true, the iframe is resized to match the dashboard content height (no inner scroll). Default false (iframe fills its container at 100%). See Auto height | | syncUrl | boolean | No | Keep the host's browser URL in sync with the dashboard's in-app route automatically, via the URL hash (e.g. #/payments/abc) — no host code required. Default true; set false to opt out (e.g. if the host owns routing). See Navigation | | ui | EmbedUi | No | Declarative UI config announced to the dashboard on init — hide/show dashboard UI elements for this embed (e.g. the checkout publish button). See Commands & UI config |

Methods

  • setTheme(theme) — Update colors, typography, mode, or external styles
  • setLang(lang) — Change display language
  • setToken(token) — Update the auth token via PostMessage
  • navigate(path) — Navigate to a dashboard route
  • dispatch(command) — Send an imperative command into the dashboard (e.g. publish the open checkout). See Commands & UI config
  • selectCheckout(checkoutCode) — Open the Checkout Builder on a specific custom checkout instance. An empty/blank code is ignored with a console warning (it could never be confirmed). See Selecting a custom checkout instance
  • setTestMode(enabled) — Toggle test/sandbox mode at runtime via PostMessage. See Test mode
  • destroy() — Remove iframe and clean up event listeners

Types

interface ThemeColors {
  primary: string;
  primaryForeground: string;
  secondary: string;
  secondaryForeground: string;
  background: string;
  foreground: string;
  muted: string;
  mutedForeground: string;
  accent: string;
  accentForeground: string;
  destructive: string;
  destructiveForeground: string;
  border: string;
  input: string;
  ring: string;
  surface: string;
  card: string;
  cardForeground: string;
  popover: string;
  popoverForeground: string;
  success: string;
  warning: string;
  info: string;
}

interface ThemeTypography {
  fontFamily: string;
  fontUrl: string;
}

interface ModeTokens {
  light?: Partial<ThemeColors>;
  dark?: Partial<ThemeColors>;
}

interface DashboardTheme {
  tokens?: Partial<ThemeColors> | ModeTokens; // flat OR per-mode
  typography?: Partial<ThemeTypography>;
  mode?: "light" | "dark";
  styles?: string; // Custom CSS injected into the dashboard
}

type EmbedEventStatus = "loading" | "success" | "error";

interface ConnectionCreatePayload {
  providerId: string;
  connectionCode?: string; // Only on "success" — the connection Yuno saved
  connectionName?: string; // On every status — the name the user typed
  reason?: string; // Only on "error"
}

interface ConnectionCreateEvent {
  status: EmbedEventStatus;
  payload?: ConnectionCreatePayload;
}

interface ConnectionDeletePayload {
  connectionCode: string;
  reason?: string; // Only on "error"
}

interface ConnectionDeleteEvent {
  status: EmbedEventStatus;
  payload?: ConnectionDeletePayload;
}

interface CheckoutPublishedPayload {
  code: string; // Custom-checkout instance UUID — present on every status
  reason?: string; // On "error": e.g. "user-cancelled" (confirmation dismissed)
  [key: string]: unknown; // On "success": the published configuration body
}

interface CheckoutPublishedEvent {
  status: EmbedEventStatus;
  payload?: CheckoutPublishedPayload;
}

interface CheckoutPublishAvailabilityPayload {
  code?: string; // Instance in context; absent on the custom-checkouts list
  isPublishAvailable: boolean; // Would publishing do anything right now?
  hasUnsavedChanges: boolean; // Blocked because styling edits need a Save first
}

interface CheckoutPublishAvailabilityEvent {
  payload: CheckoutPublishAvailabilityPayload;
}

// Custom-checkout INSTANCE lifecycle — see "Custom checkout lifecycle events".
type CustomCheckoutStatus = "PUBLISHED" | "NOT_PUBLISHED" | "ARCHIVED";

type CustomCheckoutLifecycleAction =
  | "created"
  | "published"
  | "unpublished"
  | "archived"
  | "unarchived"
  | "setAsDefault";

interface CustomCheckoutPayload {
  // The custom checkout object, as returned by the dashboard API:
  code: string; // The instance UUID
  name: string;
  description?: string; // Optional — the API manages it; the dashboard doesn't use it
  status: CustomCheckoutStatus; // Authoritative post-transition instance status
  is_default: boolean; // Post-transition default flag
  created_at: string; // ISO 8601
  updated_at?: string; // ISO 8601 — use as the change timestamp (when present)
  last_used_at?: string | null; // Last transaction, null if never used
}
// The event that fired is the envelope's action (events.customCheckout.<action>),
// so it is not repeated in the payload.

// Deprecated alias (pre-1.16.2 name) — will be removed in the next major:
type CustomCheckoutLifecyclePayload = CustomCheckoutPayload;

interface CustomCheckoutLifecycleEvent {
  status: EmbedEventStatus; // Transport status — always "success" (fires once)
  payload?: CustomCheckoutPayload;
}

// Custom-checkout SELECTION — see "Instance selection events".
interface CustomCheckoutSelectedEvent {
  status: EmbedEventStatus; // Transport status — always "success" (fires once)
  // Same shape as the lifecycle payload, but `status` is the instance's
  // CURRENT status (not a post-transition value).
  payload?: CustomCheckoutPayload;
}

interface NavigationChangedEvent {
  path: string; // The new in-app route, e.g. "/payments/abc?status=approved"
}

interface DashboardEvents {
  connections?: {
    create?: (event: ConnectionCreateEvent) => void | Promise<void>;
    delete?: (event: ConnectionDeleteEvent) => void | Promise<void>;
  };
  // Publishing a checkout's CONFIGURATION (styling, payment methods).
  checkout?: {
    published?: (event: CheckoutPublishedEvent) => void | Promise<void>;
    // Publish-button STATE (not a publish) — see "Publish availability".
    publishAvailabilityChanged?: (
      event: CheckoutPublishAvailabilityEvent,
    ) => void | Promise<void>;
  };
  // Custom-checkout INSTANCE lifecycle (create / publish / unpublish / archive /
  // unarchive / set-as-default). Distinct from checkout.published above.
  customCheckout?: {
    created?: (event: CustomCheckoutLifecycleEvent) => void | Promise<void>;
    published?: (event: CustomCheckoutLifecycleEvent) => void | Promise<void>;
    unpublished?: (event: CustomCheckoutLifecycleEvent) => void | Promise<void>;
    archived?: (event: CustomCheckoutLifecycleEvent) => void | Promise<void>;
    unarchived?: (event: CustomCheckoutLifecycleEvent) => void | Promise<void>;
    setAsDefault?: (event: CustomCheckoutLifecycleEvent) => void | Promise<void>;
    // Selection (not lifecycle): the merchant switched the instance in context.
    selected?: (event: CustomCheckoutSelectedEvent) => void | Promise<void>;
  };
}

// Imperative command sent into the dashboard via dashboard.dispatch().
type EmbedCommand =
  | {
      domain: "checkout";
      action: "publish";
      payload?: unknown;
    }
  | {
      domain: "checkout";
      action: "select"; // or the selectCheckout() convenience method
      payload: { checkout_code: string };
    }
  | {
      domain: "connections";
      action: "create.save" | "create.cancel";
      payload?: unknown;
    };

// Declarative UI config passed via the `ui` option.
interface EmbedUi {
  checkout?: {
    hidePublishButton?: boolean;
  };
  connections?: {
    hideSuccessScreen?: boolean;
    hostControlledCreation?: boolean; // you own the save moment — see the gate below
  };
  payments?: EmbedPaymentsConfig; // payment / payout detail pages
  branding?: EmbedBrandingConfig; // host logo replacing the Yuno brand
  menu?: EmbedMenuConfig; // left navigation (sidebar)
  topBar?: EmbedTopBarConfig; // top bar + banners
}

// Payment and payout detail pages (/payments/:id, /payouts/:id), for hosts that
// render their own equivalent around the iframe.
interface EmbedPaymentsConfig {
  hideSummaryCard?: boolean; // the whole card at the top of the page
  hideDetailTabs?: boolean; // the horizontal section-nav tabs below it
}

// Host branding, applied wherever the dashboard would show its own Yuno brand
// (not scoped to one section). `logo` is an image URL that replaces the Yuno
// logo; omit it and the dashboard drops the Yuno brand instead of showing it
// inside the host.
interface EmbedBrandingConfig {
  logo?: string;
}

// Left navigation. Hidden by default when embedded; `visible: true` brings it
// back. `sections` makes it a tailored, ordered allowlist.
interface EmbedMenuConfig {
  visible?: boolean;
  sections?: EmbedMenuSection[];
}
// One tailored section. Default icon dropped; pass `icon` (image URL) to show a
// custom one. `children` is an ordered allowlist of submenu ids (omit = all).
interface EmbedMenuSection {
  id: string;
  icon?: string;
  children?: EmbedMenuSection[];
}
// Top bar. `visible: true` brings it back; its parts are opt-out (each shows
// unless set to false).
interface EmbedTopBarConfig {
  visible?: boolean;
  sectionName?: boolean;
  breadcrumb?: boolean;
  testMode?: boolean;
  notifications?: boolean;
  userMenu?: boolean | EmbedUserMenuConfig;
}
// The account dropdown. Opt-out per entry; `accounts` is the org switcher.
interface EmbedUserMenuConfig {
  visible?: boolean;
  accounts?: boolean;
  profile?: boolean;
  security?: boolean;
  settings?: boolean;
  team?: boolean;
}

onNavigationChange is a top-level lifecycle callback (like onReady), not a DashboardEvents entry: navigation is a cross-cutting notification with no domain ownership, so it does not announce a host capability. See Navigation.

Session timeouts

By default, sessions issued by POST /v1/external/authenticate are valid for 24 hours. You can issue a shorter session by passing timeout_seconds (between 60 and 86400):

curl -X POST https://api.y.uno/v1/external/authenticate \
  -H "x-organization-code: <your-org-uuid>" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"<user-uuid>", "timeout_seconds": 1800}'

When the embedded session expires, the iframe paints a "Session expired" overlay and emits a message to the host. Subscribe via onSessionExpired:

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token: initialToken,
  onSessionExpired: async () => {
    const newToken = await myBackend.requestEmbedToken({ timeout_seconds: 1800 })
    sdk.setToken(newToken)
  },
})

Embed events

The embedded dashboard sends business events to the host through the events config, grouped by MFE (domain) and action. Register a handler only for what you care about:

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token: initialToken,
  events: {
    connections: {
      create: (event) => {
        // event.status is "loading" | "success" | "error"
        // event.payload is { providerId, connectionName?, connectionCode?, reason? }
        if (event.status === "success") {
          router.push(`/integrations/${event.payload!.connectionCode}/done`)
        }
      },
    },
    checkout: {
      published: (event) => {
        if (event.status === "success") {
          console.log("Checkout published", event.payload)
        }
      },
    },
  },
})

Events are notifications; ui makes the dashboard step aside

Events are pure callbacks — providing a handler tells the dashboard nothing and does not change its behavior. Making the dashboard skip its own UI is a separate, explicit decision via the ui option.

For connection creation the two compose: register events.connections.create to be notified as the creation progresses, and set ui: { connections: { hideSuccessScreen: true } } to stop the dashboard from showing its own success screen so you can route the user yourself. Each works independently — notify without suppressing, or suppress without notifying.

Controlling connection creation (the create gate)

By default the dashboard persists a connection as soon as the user clicks Save. If you must run your own processing first — and be able to stop the save when it fails, so the two systems can't drift apart — set ui: { connections: { hostControlledCreation: true } }.

The dashboard then defers the save and hands you the decision:

  1. The user clicks Save. The dashboard emits events.connections.create with status: "loading" and keeps its form locked and spinning — to the user this looks like a normal save in progress. The payload carries the providerId and the connectionName the user typed, so you can label your own record before anything is persisted.
  2. You run your processing.
  3. Reply with a command — the dashboard is waiting and will not save until you do:
    • dispatch({ domain: "connections", action: "create.save" }) — your side succeeded, persist the connection.
    • dispatch({ domain: "connections", action: "create.cancel" }) — your side failed, save nothing and unlock the form so the user can retry.
  4. The dashboard reports the outcome with status: "success" (payload adds the connectionCode Yuno minted) or "error" (payload adds a reason). Both repeat the providerId and connectionName from step 1, so you can match the outcome to the record you created without holding state yourself.
initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard"),
  ui: { connections: { hostControlledCreation: true } },
  events: {
    connections: {
      create: async (event) => {
        if (event.status === "loading") {
          try {
            await createMyObject(
              event.payload.providerId,
              event.payload.connectionName,
            );
            getDashboard().dispatch({
              domain: "connections",
              action: "create.save",
            });
          } catch {
            getDashboard().dispatch({
              domain: "connections",
              action: "create.cancel",
            });
          }
        }
        if (event.status === "success") {
          console.log(
            "Connection saved",
            event.payload.connectionCode,
            event.payload.connectionName,
          );
        }
      },
    },
  },
});

You must always reply. The dashboard has no timeout: if neither create.save nor create.cancel arrives, the form stays locked. Always create.cancel on failure.

Deleting is reported, not gated: events.connections.delete fires with loading / success / error (single-row and bulk deletes alike) so you can keep your side in sync, but you cannot veto a deletion.

Status lifecycle

Handlers fire once per state transition: first status: "loading" when the action starts, then "success" or "error" once the dashboard's call resolves. event.payload is present on every status — what it carries grows as the action progresses (identifying fields from the start, connectionCode once Yuno saved, reason on failure), so check the field you need rather than assuming. (Custom-checkout lifecycle events below are the exception — they fire once, on the completed transition.)

One more exception: if the merchant dismisses the publish confirmation dialog, checkout.published fires a single terminal "error" with payload.reason === "user-cancelled" and no preceding "loading" — nothing was ever sent to the backend. It is the only status sequence that does not start with "loading", so drive your own button off the terminal statuses rather than assuming a "loading" always came first.

Publish availability

If you hide the dashboard's own Publish button (ui.checkout.hidePublishButton) and render your own, events.checkout.publishAvailabilityChanged tells you when publishing is actually possible, so your button can behave exactly like the built-in one instead of always looking clickable.

Two events drive the button: publishAvailabilityChanged says whether you can publish, and published says whether a publish is running. Keep both in state and render from one place, so they can't overwrite each other.

let availability = null; // last reported state
let isPublishing = false;

function render() {
  if (isPublishing) {
    myPublishButton.disabled = true;
    myPublishButton.textContent = "Publishing…";
    return;
  }
  myPublishButton.textContent = "Publish";
  myPublishButton.disabled = !availability?.isPublishAvailable;
  myPublishButton.title = availability?.hasUnsavedChanges
    ? "Save your styling changes before publishing"
    : "";
}

initDashboard({
  // ...
  ui: { checkout: { hidePublishButton: true } },
  events: {
    checkout: {
      publishAvailabilityChanged: ({ payload }) => {
        availability = payload;
        render();
      },
      published: ({ status }) => {
        isPublishing = status === "loading";
        // A successful publish leaves nothing new to publish. Apply that now, so
        // the button can't be clicked again before the next availability event.
        if (status === "success" && availability) {
          availability = { ...availability, isPublishAvailable: false };
        }
        render();
      },
    },
  },
});

It fires once on load — so you have a state to render before the merchant touches anything — and then on every change. It is deduplicated: you get a call when the state actually changes, not on every edit.

isPublishAvailable is false whenever dispatching { domain: "checkout", action: "publish" } would do nothing — including when there is nothing new to publish, when changes still need saving, and on the custom-checkouts list, where the publish command is ignored. Mirroring it onto your button therefore cannot leave you with a button that silently does nothing.

hasUnsavedChanges distinguishes the two reasons publishing is unavailable: true means the merchant has styling / payment-link edits that must be saved inside the iframe first (there is no command to save on their behalf — only they can), so it is worth surfacing as a hint rather than a plain disabled button. false with isPublishAvailable: false simply means nothing changed since the last publish.

Two details to get right, both handled in the example above:

  • A cancelled publish sends no "loading". Dismissing the confirmation dialog emits a single terminal "error", so only start your spinner on "loading".
  • A failed publish sends no new availability event — nothing changed. Restore the button from the state you already have instead of waiting for one.

Custom checkout lifecycle events

For white-label hosts that embed the Checkout Builder / Custom Checkouts and render their own controls outside the iframe (e.g. an outer "Publish" button), the dashboard emits an event on every custom-checkout instance lifecycle change, so the host can keep its own button / label / list in sync with what the merchant does inside the iframe.

These are grouped under the customCheckout domain, separate from checkout.published:

  • checkout.published — publishing a checkout's configuration (styling, payment methods). Follows the loading → success/error lifecycle above. event.payload.code carries the custom-checkout instance UUID on every status (the same identifier as customCheckout.* payloads), so you can track which instance was published — including the first publish of a newly provisioned instance, where no other event has told you the id yet. On success the payload also carries the published configuration body (name, config, and styling when styling changed); loading carries only code, and error carries code plus an optional reason ("user-cancelled" when the merchant dismissed the confirmation dialog). Type: CheckoutPublishedPayload.

    events: {
      checkout: {
        published: (event) => {
          if (event.status === "loading") {
            // The merchant confirmed — the publish is now in flight.
            outerPublishButton.setLoading(true);
            return;
          }
    
          outerPublishButton.setLoading(false);
    
          if (event.status === "success") {
            // The instance that was just published — available even on the
            // very first publish.
            trackPublishedInstance(event.payload!.code);
            return;
          }
    
          // status === "error". A dismissed confirmation dialog lands here too,
          // with no "loading" before it — nothing was sent to the backend.
          if (event.payload?.reason === "user-cancelled") return;
          showPublishError();
        },
      },
    },
  • customCheckout.* — the instance lifecycle (the checkout object itself): created, published, unpublished, archived, unarchived, setAsDefault.

Every customCheckout event carries the same payload — the fields of the custom checkout object — and fires once, on the completed transition (event.status is always "success"):

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  path: "/checkout-builder",
  events: {
    customCheckout: {
      created: (event) => {
        // A new instance exists (PUBLISHED, is_default false).
        console.log("Created", event.payload!.code);
      },
      published: (event) => {
        // Instance went live — flip the outer "Publish" button to "Published".
        outerPublishButton.setPublished(true);
      },
      unpublished: (event) => outerPublishButton.setPublished(false),
      archived: (event) => refreshMyCheckoutList(),
      unarchived: (event) => refreshMyCheckoutList(),
      setAsDefault: (event) => {
        // event.payload.is_default === true, status === "PUBLISHED".
        markAsDefault(event.payload!.code);
      },
    },
  },
});

| Event | Transition | Payload status | Payload is_default | | --- | --- | --- | --- | | created | new instance | PUBLISHED | false | | published | NOT_PUBLISHED → PUBLISHED | PUBLISHED | unchanged | | unpublished | PUBLISHED → NOT_PUBLISHED | NOT_PUBLISHED | unchanged | | archived | → ARCHIVED | ARCHIVED | false | | unarchived | ARCHIVED → NOT_PUBLISHED | NOT_PUBLISHED | unchanged | | setAsDefault | is_default → true (auto-publishes) | PUBLISHED | true |

Payload (CustomCheckoutPayload) is the custom checkout object as the API returns it, with status set to the authoritative post-transition value. Same shape for every event:

{
  code: "52c2f00b-92af-4ded-8fba-644613464d19", // the instance UUID
  name: "Checkout LATAM",
  description: "…",            // optional — the API manages it; unused by the dashboard
  status: "PUBLISHED",         // PUBLISHED | NOT_PUBLISHED | ARCHIVED
  is_default: true,            // exactly one default per account
  created_at: "2026-07-08T13:51:42.869114Z",
  updated_at: "2026-07-14T16:28:15.275382Z", // optional — use as the change timestamp
  last_used_at: "2026-07-15T15:39:14.267177Z", // optional — null if never used
}
// The action (e.g. "setAsDefault") is the envelope's action, not a payload field.

setAsDefault demotion: promoting a new default demotes the previous one (is_default → false) server-side. Only one event is emitted — for the newly-promoted instance. The demoted instance does not get its own event; refresh your list off the setAsDefault event if you track the previous default.

Host/iframe contract: the embedded dashboard posts { type: "yuno-dashboard:embed-event", domain: "customCheckout", action, status: "success", payload } to the host on each transition. These events fire only in the embedded (white-label) context — no postMessage traffic for standard dashboard users.

Instance selection events

customCheckout.selected complements the lifecycle events above: it reports which instance the merchant is working on, not a state change. It fires whenever the selected checkout instance changes, including the initial load:

  • on load, when the builder auto-selects the instance to open — so you always learn which instance is in context, even though the merchant may have changed which instance is the account default;
  • the merchant picks another instance in the builder's top-menu dropdown;
  • the merchant opens one from the Custom Checkouts table ("Open in editor");
  • the selection is replaced automatically — an account switch swaps in the new account's default instance, or the open instance is unpublished/archived and the default takes over;
  • the host drives the selection with selectCheckout(code) — honored selects report "success" with the instance payload; a rejected code reports "error" with only the rejected { code }.

Re-picking the already-open instance does not re-fire.

The payload is the same custom checkout object as the lifecycle events (code, name, status, is_default, …), except status is the instance's current status, not a post-transition value.

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  path: "/checkout-builder",
  events: {
    customCheckout: {
      selected: (event) => {
        // Render the implementation guide / security preferences for the
        // instance now in context.
        showImplementationGuide(event.payload!.code);
      },
    },
  },
});

Navigation (keeping the host URL in sync)

When the user navigates inside the iframe (e.g. Payment Detail → Routing), the SDK keeps the host's browser URL in sync automatically — you write no code. This is on by default (syncUrl: true) and works in both directions via the URL hash:

  • iframe → host: on every in-app route change the SDK writes the route to the host URL hash, e.g. https://your-app.com/dashboard#/payments/abc. The address bar stays shareable and bookmarkable, and each navigation is a history entry.
  • host → iframe (reload / shared link): on load the SDK reads the route from the hash and boots the iframe straight into that view, so a reload or a shared bookmark reopens where the user was.
  • host → iframe (back / forward): the SDK listens for the host URL changing (browser back/forward) and steers the dashboard to match.
// That's it — bookmarking, sharing, and back/forward just work:
const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token: initialToken,
});

Set syncUrl: false to opt out — for example if the host owns routing and prefers to manage the URL itself. In that case (or alongside the automatic sync) you can register the optional top-level onNavigationChange callback to run custom logic on each navigation:

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token: initialToken,
  syncUrl: false, // SDK won't touch the host URL
  onNavigationChange: (event) => {
    // event.path is the dashboard's new in-app route
    window.history.replaceState(null, "", `/embed${event.path}`);
  },
});

onNavigationChange is a pure notification — like onReady it is a top-level lifecycle callback (not a domain event), it has no status lifecycle, and the dashboard does not change its own behavior; it just reports the new route via event.path.

Host/iframe contract: the embedded dashboard posts { type: "yuno-dashboard:embed-event", domain: "navigation", action: "changed", payload: { path } } to the host on each internal navigation; the SDK reflects path into the host URL hash (when syncUrl) and routes it to the optional callback. Messages are accepted only from the configured baseUrl origin.

Commands & UI config

The previous sections cover the dashboard handing off to the host. These two channels go the other way — the host drives the dashboard:

  • Commands (imperative): trigger an action inside the dashboard, e.g. publish the checkout the user is editing.
  • UI config (declarative): hide/show dashboard UI elements for this embed, e.g. hide the dashboard's own publish button so the host can render its own.

Commands — dispatch(command)

Navigate to the relevant view first, then dispatch. A command for an MFE that is not currently mounted is a no-op (nothing is queued for it), so make sure the target view is open. The result of the action comes back through the matching embed event — e.g. publishing reports via events.checkout.published.

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  path: "/checkout/editor/abc",
  events: {
    checkout: {
      published: (event) => {
        if (event.status === "success") console.log("Published!", event.payload);
      },
    },
  },
});

// Host's own "Publish" button:
publishBtn.addEventListener("click", () => {
  sdk.dispatch({ domain: "checkout", action: "publish" });
});

checkout.publish asks for confirmation. It does not publish immediately: the dashboard opens its own publish confirmation dialog — the same one its internal Publish button shows — and only publishes once the merchant confirms. So events.checkout.published "loading" arrives after the confirmation, not when you dispatch, and if the merchant dismisses the dialog you get a single "error" with payload.reason === "user-cancelled" instead. Keep this in mind if you put your button into a pending state on click: wait for "loading" before showing progress, and clear it on either terminal status. Dispatching this command from an unattended/scripted flow will simply wait for a human.

Selecting a custom checkout instance — selectCheckout(code)

Breaking in 2.0.0: CustomCheckoutSelectedEvent.payload is now CustomCheckoutPayload | Pick<CustomCheckoutPayload, "code"> — on status: "error" (a rejected select) only the rejected code is known. Existing selected handlers that read payload.name / payload.status must narrow on event.status first, as shown below. Runtime behaviour for hosts that never call selectCheckout is unchanged.

When the host manages its own instance list outside the iframe (white-label), the builder normally opens on the account default. selectCheckout opens it on a specific instance instead — the inbound counterpart of the customCheckout.selected event:

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  path: "/checkout-builder",
  events: {
    customCheckout: {
      selected: (event) => {
        if (event.status === "error") {
          // The code was not honored (unknown / not openable for the
          // authenticated account): the builder stays on / falls back to the
          // default, and event.payload carries only the rejected { code }.
          console.warn("select rejected", event.payload?.code);
          return;
        }
        showImplementationGuide(event.payload!.code);
      },
    },
  },
});

// On load — dispatch right after init; the builder replays the last select
// once it mounts:
sdk.selectCheckout("cc_123");

// At runtime — the user picks another instance in the host's own list:
instanceList.addEventListener("change", (e) => sdk.selectCheckout(e.value));

Unlike other commands, checkout.select is not lost when the builder view is still loading: the dashboard replays the last select once the builder mounts, so dispatching immediately after initDashboard works. The selection is confirmed (or rejected) through events.customCheckout.selected either way. The code must belong to the authenticated account — a foreign or unknown code never selects anything and reports status "error".

UI config — ui

Announced once to the dashboard when it becomes ready. Use it to suppress dashboard UI that the host replaces with its own:

initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  ui: {
    checkout: {
      hidePublishButton: true, // host renders its own publish button
    },
    connections: {
      hideSuccessScreen: true, // host owns the post-create moment
    },
  },
});

Hiding the publish button does not hide the publish confirmation dialog: when your own button dispatches checkout.publish the dashboard still asks the merchant to confirm, and reports a dismissal as checkout.published "error" with payload.reason === "user-cancelled".

Payment detail pages. The detail page for a payment (/payments/:id) and a payout (/payouts/:id) opens with a card summarising the transaction, and a row of horizontal tabs that scroll to each section below it. Hide either when your own page already shows that information:

initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  ui: {
    payments: {
      hideSummaryCard: true, // the card at the top of the page
      hideDetailTabs: true, // the horizontal section-nav tabs
    },
  },
});

hideSummaryCard removes the entire card: the amount, the action buttons (capture / cancel / refund / download PDF), the status badges, and the id / date / country grid — the page then starts at the timeline. If you hide it, your own UI has to provide any of those actions you still want the user to have. Both flags default to off, and both apply to payments and payouts alike.

Menu & top bar (white-label). Embedded mode hides the dashboard's own navigation menu and top bar by default — the host owns the frame. To bring either back (e.g. a white-label host that wants Yuno's navigation), opt it in:

initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  ui: {
    menu: { visible: true }, // show the left navigation (sidebar)
    topBar: { visible: true }, // show the top bar + banners
  },
});

Both are opt-in: omit them (or set visible: false) to keep the current no-chrome embed.

Tailored menu & top bar (white-label). Go further and render only the sections a white-label customer needs, in your order, with the top-bar parts you want. The menu is an ordered allowlist (icons dropped unless you pass an image URL); the top-bar parts are opt-out (each shows unless set to false):

initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  ui: {
    menu: {
      visible: true,
      sections: [
        { id: "home", icon: "https://cdn.acme.com/home.svg" },
        { id: "connections", children: [{ id: "providers" }] }, // only this submenu
        { id: "routing" },
      ],
    },
    topBar: {
      visible: true,
      testMode: false, // hide these parts…
      notifications: false,
      userMenu: { accounts: false, team: false }, // …and these account entries
      // sectionName / breadcrumb stay on (opt-out)
    },
  },
});

Menu section ids: home, insights, operations, reconciliations, connections, routing, checkout-builder, installments, risk-conditions, marketplace, subscriptions, payment-links, payments-concierge, nova, api-reference, audit-logs. A configured id the customer can't access (permission / feature flag) is skipped.

Branding (host logo). Wherever the dashboard would show its own Yuno brand inside the frame (e.g. the connection create/edit screen shows the provider logo linked to the Yuno logo), pass your own logo to replace it:

initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  ui: {
    branding: {
      logo: "https://cdn.acme.com/logo.svg", // replaces the Yuno logo
    },
  },
});

Omit branding.logo and, when embedded, the dashboard drops the Yuno brand entirely (logo and its connector icon) rather than showing it inside the host.

Host/iframe contract: dispatch() posts { type: "yuno-dashboard:command", domain, action, payload } to the iframe; the ui config is announced as { type: "yuno-dashboard:ui", ui } on ready. The dashboard validates both come from the host (parent) origin.

Test mode

The dashboard can run in test/sandbox mode or live mode. Set it at mount time via the testMode option, or toggle it at runtime via setTestMode(enabled):

const sdk = initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  path: "/connections", // clean route — no query string
  testMode: false,      // mount in live mode
});

// Later — flip to test mode without remounting
sdk.setTestMode(true);

Do not pass test mode through path. The SDK builds the iframe URL by appending its own query string (embed, theme, lang, and test when testMode is set) to the route: ${baseUrl}${path}?embed=true&…. A path that already carries a query (e.g. "/connections?test=false") produces a malformed double-? URL that swallows embed and breaks the embed (the dashboard never mounts). Always pass a query-free path and use the testMode option / setTestMode() method instead.

Host/iframe contract: when testMode is provided, the SDK appends test=<bool> to the initial iframe URL — the embedded dashboard reads it from its own URL on load. For the runtime toggle, setTestMode(enabled) sends postMessage({ action: "setTestMode", testMode }) (accepted only from the configured baseUrl origin); the embedded dashboard must handle that action to switch mode without a reload.

Loading overlay

A loading overlay covers the iframe while initialization completes. You can provide a custom element via the loading config option, or the SDK renders a default spinner. The overlay fades out automatically (300ms transition) once the dashboard is authenticated and ready.

Auto height

By default the iframe fills its container (height: 100%) and the dashboard content scrolls inside it. Pass autoHeight: true to size the iframe to the dashboard's content height instead, so the whole host page scrolls naturally with no inner scrollbar:

initDashboard({
  baseUrl: "https://dashboard.y.uno",
  container: document.getElementById("dashboard")!,
  token,
  autoHeight: true,
});

Host/iframe contract: when autoHeight is enabled, the embedded dashboard reports its content height to the host via postMessage({ action: "resize", height }) whenever the content resizes; the SDK applies that height to the iframe and its wrapper. Messages are accepted only from the configured baseUrl origin. Your container must allow vertical growth (avoid a fixed height / overflow: hidden) for the effect to be visible.

Overlays (modals & drawers)

An embedded overlay is positioned relative to its iframe, not the host page — so a modal would appear centered to the iframe panel, not the browser viewport. To fix this, the embedded dashboard signals the SDK when an overlay opens or closes (embed-event with domain: "ui", action: "overlay-open" | "overlay-close"), and the SDK expands the iframe to the full viewport (position: fixed; inset: 0, top z-index) for as long as any overlay is open, then restores it. This is automatic — no host code required. Signals are ref-counted, so stacked overlays are handled correctly.

While expanded the iframe is transparent, and the SDK captures the iframe's prior panel position (getBoundingClientRect) and sends it to the dashboard as an offset (postMessage({ action: "overlay-mode", active, offset })). The dashboard uses that offset to leave the host's chrome regions unpainted, so your surrounding UI stays visible through the iframe while the overlay is centered to the page. No host code required.

Host/iframe contract: while an overlay is open the iframe covers the whole viewport (transparent) at a maximal z-index. Host chrome behind the iframe is visible but not interactive during the overlay (the iframe captures pointer events) — expected for a modal. Messages are accepted only from the configured baseUrl origin.

Checkout preview (initCheckoutPreview)

A read-only preview of a published custom checkout, in its own iframe: the payment method list and the form as your merchant's buyers will see them, with the styling, payment-method visibility and required fields of a given checkout code.

Nothing behind it can take a payment. There is no checkout_session and no transaction token; the Pay / Save CTA is inert, and the frame makes no transaction call.

import { initCheckoutPreview } from '@yuno-payments/dashboard-embed-sdk'

const preview = initCheckoutPreview({
  baseUrl: 'https://dashboard.y.uno',
  container: document.getElementById('preview')!,
  token: dashboardJwt,
  checkoutCode: 'cc_9f2a...',   // the code from checkout.published / customCheckout.*
  accountCode: 'acc_1b7c...',   // the account that checkout belongs to (see below)
  testMode: false,              // live data — the frame defaults to TEST
  platform: 'desktop',          // 'desktop' | 'mobile' — default 'desktop'
  onHeight: ({ height }) => {
    // Position your own content directly under the frame.
    brandingEl.style.marginTop = `${height}px`
  },
  events: {
    checkoutPreview: {
      loaded: ({ status, payload }) => {
        if (status === 'error') hidePanel(payload.reason)
      },
    },
  },
})

Not a second dashboard

initDashboard is a singleton and throws on a second live instance — a guard worth keeping, since re-initializing on every navigation reloads the iframe and loses the route. A preview is a different thing, so it has its own entry point:

  • initCheckoutPreview is not a singleton. Mount as many previews as you need, alongside a running dashboard instance.
  • getDashboard() keeps meaning "the dashboard".
  • Each instance only acts on messages from its own iframe, so two frames on the same origin never receive each other's events or resize each other.

Your preview frame needs its own token. It is a separate iframe with its own session — a token set on your dashboard instance does not reach it. Pass token on init, or call preview.setToken(jwt).

Multi-account users: pass accountCode

A checkout code is resolved account-scoped — the same code means nothing outside the account that owns it. And a preview frame cannot inherit which account the user is working in: an iframe is a separate browsing context with partitioned storage, so it starts empty even when the same user has a dashboard tab open on the right account. Left to itself the frame falls back to the user's principal account, and a checkoutCode living in any other account fails to resolve — the frame reports checkoutPreview.loaded with status: 'error'.

So whenever your users can have more than one account, pass the one the checkout belongs to:

initCheckoutPreview({
  baseUrl: 'https://dashboard.y.uno',
  container: el,
  token: dashboardJwt,
  checkoutCode: 'cc_9f2a...',
  accountCode: 'acc_1b7c...',
})

The dashboard applies it on load, before the first account-scoped request, and only if the authenticated user actually has access to that account — a code they cannot reach is ignored rather than trusted, and the frame resolves the account on its own as before. Single-account users can leave it out.

accountCode and testMode work together. An account has two environment codes, and the frame resolves checkoutCode with the one matching its mode. An embedded frame defaults to test, so a checkout published in live needs testMode: false — otherwise the lookup runs against the account's testing environment and comes back not-found. Both are applied on load, so changing either means mounting a new preview.

Options

| Option | Type | Required | Description | | --- | --- | --- | --- | | baseUrl | string | Yes | Dashboard origin, e.g. https://dashboard.y.uno. | | container | HTMLElement | Yes | Element the preview iframe is mounted into. | | checkoutCode | string | Yes | The custom checkout to render. The only configuration input — the preview always shows that code's published configuration, and individual values cannot be overridden. | | token | string | No | Dashboard JWT. Required to render; pass it here or via setToken(). | | accountCode | string | No | The account checkoutCode belongs to. Pass it whenever the user has more than one account — see below. Accepts the account's identity code or either environment code. | | platform | 'desktop' \| 'mobile' | No | Which form width to render. Default 'desktop'. | | testMode | boolean | No | Which data the frame runs on. Defaults to TEST (the embedded default) — pass false for a live checkout, or the frame looks for the code in the account's testing environment and reports not-found. | | autoHeight | boolean | No | Size the iframe to the rendered preview. Default true (unlike initDashboard) — a preview has no chrome of its own. | | onHeight | (e: CheckoutPreviewHeightEvent) => void | No | The rendered height. See below. | | events.checkoutPreview.loaded | (e: CheckoutPreviewLoadedEvent) => void | No | Outcome of resolving the code. | | lang, theme, loading, onReady, onSessionExpired | — | No | Same meaning as on initDashboard. |

Methods

  • setToken(jwt) — supply or refresh the preview's dashboard token.
  • setCheckoutCode(code) — point the frame at another published checkout. Counts as a new rendered configuration, so loaded and onHeight each fire again.
  • setPlatform('desktop' | 'mobile') — switch the rendered width.
  • setLang(lang) / setTheme(theme) — as on a dashboard instance.
  • destroy() — tear down the iframe and its listeners.
  • isDestroyed() — whether destroy() has run.

Height (onHeight)

Fires once per rendered configuration, right after the preview paints — so you can butt your own content against the frame without a gap. The value is the preview's own height in CSS pixels, including its internal spacing and excluding any dashboard chrome.

It is not a resize stream. Interacting with the form (opening a payment method, expanding the condensed view) changes the height without re-emitting. With autoHeight on (the default) the iframe still resizes itself, so if you lay out in normal document flow you do not need onHeight at all — it is for hosts that position content explicitly. Calling setCheckoutCode() or setPlatform() produces a new configuration, and a new emission.

It fires whether or not autoHeight is on.

Outcome (events.checkoutPreview.loaded)

loading on mount, then exactly one terminal status. The event is a discriminated union on status, so if (status === 'error') narrows payload.reason to a defined value — no non-null assertion needed. On error the payload carries a reason:

| reason | Meaning | | --- | --- | | not-found | No checkout with that code in the authenticated account. An unknown code and another account's code are indistinguishable — the lookup is account-scoped by design. | | not-published | The checkout exists but has never been published, so it has no published configuration to show. | | no-payment-methods | Published, but every payment method in it is switched off. |

The frame renders a readable state for each of these — it never goes blank — so handling the event is optional, and only needed if you want to hide or relabel your own chrome.

Limitations

The preview is the same mock-up the Checkout Builder shows, so it inherits the same limits:

  • Generic layout. Every payment method renders the same form shape; it does not reproduce each method's real flow.
  • No interactive states. No confirmations, popups or modals triggered by user action.
  • No condition filters. The Checkout Builder's country / currency / amount preview filters are not applied — the preview always shows the configuration as published.
  • Form only. The payment method list and its form, not a full payment-link page (no merchant logo, amount or order summary).

Types

type CheckoutPreviewPlatform = 'desktop' | 'mobile'

type CheckoutPreviewErrorReason =
  | 'not-found'
  | 'not-published'
  | 'no-payment-methods'

interface CheckoutPreviewPendingPayload {
  code: string
}

interface CheckoutPreviewErrorPayload {
  code: string
  reason: CheckoutPreviewErrorReason   // always present on an error
}

type CheckoutPreviewLoadedPayload =
  | CheckoutPreviewPendingPayload
  | CheckoutPreviewErrorPayload

// Discriminated on `status`: narrowing to 'error' gives you a defined
// `payload.reason`, and `reason` does not exist on the other branch.
type CheckoutPreviewLoadedEvent =
  | { status: 'loading' | 'success'; payload: CheckoutPreviewPendingPayload }
  | { status: 'error'; payload: CheckoutPreviewErrorPayload }

interface CheckoutPreviewHeightEvent {
  code: string
  height: number
}

Migrating from 0.x to 1.0

1.0.0 drops the Yuno brand prefix from the public API so the SDK reads cleanly in white-labelled integrations. The old names were removed — there is no compatibility shim, so update every import and call site:

| 0.x (removed) | 1.0 (new) | |---|---| | initYunoDashboard(config) | initDashboard(config) | | getYunoDashboard() | getDashboard() | | destroyYunoDashboard() | destroyDashboard() | | YunoDashboard (class) | Dashboard | | YunoDashboardConfig (type) | DashboardConfig |

- import { initYunoDashboard, getYunoDashboard, destroyYunoDashboard } from "@yuno-payments/dashboard-embed-sdk";
- import type { YunoDashboardConfig } from "@yuno-payments/dashboard-embed-sdk";
+ import { initDashboard, getDashboard, destroyDashboard } from "@yuno-payments/dashboard-embed-sdk";
+ import type { DashboardConfig } from "@yuno-payments/dashboard-embed-sdk";

- const sdk = initYunoDashboard(config);
+ const sdk = initDashboard(config);

Behavior, config options, methods, callbacks, and the PostMessage host/iframe contract are unchanged — this release renames the API surface only.

Development

npm install
npm run build      # Build with tsup
npm run dev        # Watch mode
npm run type-check # TypeScript check