> ## Documentation Index
> Fetch the complete documentation index at: https://docs.catenatelematics.com/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript reference

> Every option and behaviour of open(), resume() and preload() in the Connect embed SDK

`@catenaclearing/connect-sdk` exports three functions: `open`, `resume` and `preload`.

## open(options)

Launches the flow. A click handler is the usual place to call it, but the iframe needs no user gesture: the popup takes its gesture from the **Continue** button.

| Option | Type | Required |
| - | - | - |
| `inviteUrl` | `string` | yes |
| `embedKey` | `string` | yes |
| `container` | `HTMLElement` | no |
| `variant` | `"full" \| "card" \| string` | no |
| `theme` | `"light" \| "dark" \| string` | no |
| `logoUrl`, `logoUrlDark`, `brandColor`, `brandSecondary` | `string` | no |
| `onOpen`, `onConnection`, `onConnectionDeleted`, `onSuccess`, `onExit`, `onClose` | callbacks | no |

Returns `{ destroy(): void }`. See [destroy()](#destroy).

`open()` throws if it is called outside a browser, or before `<body>` exists with no `container`.

<ParamField body="inviteUrl" type="string" required>
  The invitation URL. The origin the package trusts is derived from this URL. That origin is used for the iframe, the popup, and every inbound message check. The package has no origin of its own, so pointing it at a development environment is just a different invite URL. No separate build, no configuration.

  The URL must be `https`, except on `localhost`, `127.0.0.1` and `[::1]`. Over plain http on a shared network, anyone on that network can be the origin: they can serve the flow, post a fake success, and read the embed key.

  Pass only invite URLs you created, in the Catena dashboard or through the Catena API, and hand them to the page from your own server. Never take one from your page's query string, a form field, or anything else a visitor or another site can set. Whoever chooses the URL chooses the origin the package trusts, and can do the same three things.

  `open()` throws a `TypeError` if the URL does not parse, is not http or https, or is http on any host other than `localhost`, `127.0.0.1` or `[::1]`.
</ParamField>

<ParamField body="embedKey" type="string" required>
  Your embed key, issued by Catena. It is valid only for the origins registered against it. See [What you need](/get-started/connect-sdk/overview#what-you-need).
</ParamField>

<ParamField body="container" type="HTMLElement">
  An element to mount the flow inline. Omit it and the package renders its own overlay. Behaviour is otherwise identical, including on the fallback modes, with one difference: the overlay has a close button and Escape handling, and a container has neither, so the way out of it is yours to provide. See [The package never dismisses your surface](/get-started/connect-sdk/events#the-package-never-dismisses-your-surface). The package only removes what it created, so your container is left as it was found.

  The surface fills the container and is at least 480px tall. Give the container a height of its own, at least that much. In a shorter container the surface overflows it, and in one with no height set the surface is exactly 480px.
</ParamField>

The six callbacks are all optional. [Events and outcomes](/get-started/connect-sdk/events) explains what each one means and which modes deliver it.

<ParamField body="onOpen" type="() => void">
  The flow is ready. It can arrive more than once, for example after the embedded page reloads, so make the handler safe to run twice. Not delivered in the redirect mode.
</ParamField>

<ParamField body="onConnection" type="({ connectionId: string }) => void">
  One provider was connected. The flow is still running, so do not dismiss on it. Not delivered in the redirect mode.
</ParamField>

<ParamField body="onConnectionDeleted" type="({ connectionId: string }) => void">
  The fleet deleted a connection. The flow is still running. Drop the id if you stored it from `onConnection`; it may also be a connection made before this session. Not delivered in the redirect mode.
</ParamField>

<ParamField body="onSuccess" type="({ connectionIds: string[] }) => void">
  The user finished and chose to come back to you. Mutually exclusive with `onExit`.
</ParamField>

<ParamField body="onExit" type="({ reason: string }) => void">
  The flow ended without success. Mutually exclusive with `onSuccess`. Two reasons mean the invite link never loaded: see [onExit when the invite link never loaded](/get-started/connect-sdk/events#onexit-when-the-invite-link-never-loaded).
</ParamField>

<ParamField body="onClose" type="() => void">
  The surface should be dismissed. It arrives right after `onSuccess` or `onExit`. It arrives on its own in two cases: the user closes the popup window in the popup mode (the package checks the window every half second), or the user presses the overlay's close button or Escape. Nothing inside the flow produces it alone.
</ParamField>

### Presentation options

`variant`, `theme`, `brandColor`, `brandSecondary`, `logoUrl` and `logoUrlDark` control how the flow looks. All six are optional. Omit them and the flow renders with the branding configured on your account.

```js theme={null}
open({
  inviteUrl,
  embedKey,
  variant: "card",
  theme: "dark",
  brandColor: "#d1004b",
  brandSecondary: "#5bc2e7",
  logoUrl: "https://cdn.example.com/logo.svg",
  logoUrlDark: "https://cdn.example.com/logo-dark.svg",
  onSuccess: ({ connectionIds }) => continueYourFlow(connectionIds),
});
```

Each one is forwarded to the app as a query parameter on the launch URL, exactly as you supplied it, and the app carries it across its own navigations so the appearance holds for the whole flow. The app validates them, not this package. An invalid value is ignored on its own: that one setting falls back to its default, the others still apply, and the flow still renders.

The known values autocomplete, but a value your installed version has never seen still compiles and is still sent. The app ignores values it does not recognize and uses its default, so you do not need a package release to use a new one.

<ParamField body="variant" type="&#x22;full&#x22; | &#x22;card&#x22; | string" default="full">
  Which layout the flow renders.

  * `"full"`, the default, is the flow as it renders on its own: a background, a navigation header carrying the logo, a theme toggle, help and Disconnect, and the connection card with the "Secure connection powered by Catena" attribution below it.
  * `"card"` is the connection card alone, with the attribution moved inside it. It fills whatever it is mounted in edge to edge, so the card's own background is all you see: no page background behind it and no border or rounded corners of its own. The surface around it supplies the frame. The header goes with everything else, so there is no theme toggle and no Disconnect. Help moves into the card, at the top right of each screen. The logo still appears on the invite consent card, which opens an invited session in either variant.

  The package's own overlay is a rounded panel of at most 460 by 720 pixels, and `"card"` is the fit for it and for a `container` you have already styled. The option rides on the launch URL, so it also applies when the flow falls back to a popup or a redirect, where it has a whole tab. `"full"` suits that, and a container with room for it.

  A user who needs to disconnect a provider needs the header. Use `"full"`, or send them to the flow outside your page.
</ParamField>

<ParamField body="theme" type="&#x22;light&#x22; | &#x22;dark&#x22; | string">
  Forces the color scheme. `"light"` or `"dark"` overrides the user's stored preference and the themes configured on your account, and the flow offers no theme toggle while one is set. The forced value is not stored, so the user's own preference is untouched on their next visit to the flow outside your page.

  Omit it to leave the resolution alone: the user's stored preference, then their device preference, then light.

  There is no `"auto"`. The iframe is a separate document and cannot see your page's theme. If you want the flow to match your surface, say which one you are in. If you want it to follow the user's device, pass nothing.

  The package's own surface (the loading state, the **Continue** button and the overlay's close button) is dark under `"dark"` and light under `"light"`. When you pass nothing it follows the device's preference only. It cannot see a preference the user stored in the flow, so a user whose stored choice differs from their device sees the surface in one scheme and the flow in the other. Pass a theme if they must match.
</ParamField>

<ParamField body="brandColor" type="string">
  The flow's primary color, in both light and dark: the progress indicator and the primary buttons. Six hex digits, with or without a leading `#`, so `"d1004b"` and `"#d1004b"` are the same value. Text and icons drawn on the color are derived to stay readable, so a near-white or near-black value still produces legible buttons.

  The progress indicator shows this color until the user has an active connection, then switches to the success color: `brandSecondary` if you set one, the flow's green if not. The buttons keep the brand color throughout.

  Anything else is ignored and the color configured on your account applies: three-digit shorthand, eight digits with alpha, named colors, `rgb()` and `hsl()`.
</ParamField>

<ParamField body="brandSecondary" type="string">
  The flow's success color, in both light and dark. It replaces the green that marks a connection: the progress indicator once the user has one, the border and marker on a connected provider, the "Connected" badges and the success messages. Lighter and darker shades for the badges and messages are derived from it.

  Same format as `brandColor`, and the same values are ignored. It is independent of `brandColor`: omit it and the green stays, whatever `brandColor` is.

  A darker shade of it is used for small text and icons on the card, so a very pale color reads poorly there.
</ParamField>

<ParamField body="logoUrl" type="string">
  Replaces your account's logo wherever the flow shows one: the navigation header under `variant: "full"`, and the invite consent card that opens an invited session under either variant. With `"card"` there is no header, so the logo appears on the consent card alone.

  An absolute `https` URL on any host. Any other scheme, a protocol-relative URL or a relative path is ignored.

  The user's browser fetches the image directly from the host you name. It is not proxied and not resized, though an oversized image is constrained to the logo's slot. A URL that fails to load collapses the logo area rather than showing a broken image or falling back to your account's logo.
</ParamField>

<ParamField body="logoUrlDark" type="string">
  The logo shown in the dark theme, in the same places as `logoUrl`. It falls back to `logoUrl` when omitted or invalid. The same rules apply: an absolute `https` URL on any host, fetched directly by the user's browser, and a failed load collapses the logo area.
</ParamField>

<Note>
  **Branding needs your embed key to resolve.** `brandColor`, `brandSecondary`, `logoUrl` and `logoUrlDark` are honored only when the launch's embed key resolves to a registered, active key. With an unknown or revoked key all four are ignored and the flow uses the branding configured on your account. `variant` and `theme` need no key.
</Note>

### destroy()

Removes everything the launch owns: its surface, the iframe or window it opened, its message listener and its timers. It is idempotent and safe to call after the flow has ended, so you can call it from a component teardown without checking state first.

The one thing it leaves alone is a capability probe already in flight. The probe belongs to the page, not to the launch ([preload()](#preload-options) runs one with no launch at all). It cleans itself up within its own timeout, and its result is kept so the next launch with the same embed key skips the loading state.

### One flow at a time

Calling `open()` while a launch is live brings it back to the user and returns that launch's handle. It does not start a second flow. In the popup mode, once the window is open, that means focusing the window. Otherwise it focuses the overlay, and in a container it does nothing visible.

Once a launch has ended (after `onSuccess`, `onExit` or `onClose`) and you have not called `destroy()`, `open()` replaces it: the old launch gets `onClose` if it has not had one, its surface is removed, and a new flow starts with a new handle. If that `onClose` throws, the new flow still starts, and the error is reported from its own task, the way an error thrown from any other callback is.

## resume(options)

Only relevant to the [redirect mode](/get-started/connect-sdk/how-it-works#the-three-modes), and only when your invitation has redirect URLs. Call it on the page the flow returns to.

| Option | Type | Required |
| - | - | - |
| `outcome` | `"success" \| "exit"` | yes |
| `onSuccess`, `onExit`, `onClose` | callbacks | no |

```js theme={null}
import { resume } from "@catenaclearing/connect-sdk";

// On your success destination
resume({ outcome: "success", onSuccess: ({ connectionIds }) => {} });

// On your failure destination
resume({ outcome: "exit", onExit: ({ reason }) => {} });
```

You have to state the outcome. The app signals success or failure by navigating to one of the two destination URLs on your invitation. The package is given your invite URL, not those destinations, so it cannot tell which page it woke up on. You can.

The flow appends query parameters to the destination URL. `status` is `success` on the success page. On the failure page it is `failure` when the user declined consent or disconnected, and `integration-requested` when they requested an integration or joined a waitlist, the ending that arrives as `onExit` in the other modes. `skip_reason` is added on a decline with a reason, and `tsp_count` on both.

<Warning>
  These are two separate call sites, not one call with `outcome` swapped. `resume()` calls `onExit` for `"exit"`, so copying the success form and changing only `outcome` gives you a page that never receives a terminal callback.
</Warning>

It emits the terminal callback you declared (`onSuccess` for `"success"`, `onExit` for `"exit"`), followed by `onClose`. Never `onOpen`: the flow reported itself ready on a page the navigation already destroyed.

`onExit` gets the same `reason` the other modes report for the same exit: the URL's `skip_reason` when there is one, otherwise its `status` (for example `failure` or `integration-requested`). It is empty only if the URL carries neither. `connectionIds` is always empty, because nothing on the URL lists them. Read `tsp_count` from the query yourself if you need it. Your connection [webhooks](/api-reference/webhooks/overview) are the record, as they are on every path.

Both destination pages must be on the same origin as the page that called `open()`, or `resume()` emits nothing.

With no launch in flight, or an expired one, `resume()` does nothing, so it can sit at the top of a destination page unconditionally. The flip side: a callback means a redirected launch left this tab recently, not that this page was reached from the flow. An abandoned launch stays recorded for up to 30 minutes, and a `resume()` inside that window will consume it. Put the call on your two destination pages and nowhere else. [Troubleshooting](/get-started/connect-sdk/troubleshooting#when-you-receive-nothing) covers the cases where `resume()` emits nothing.

## preload(options)

| Option | Type | Required |
| - | - | - |
| `inviteUrl` | `string` | yes |
| `embedKey` | `string` | no |

```js theme={null}
import { preload } from "@catenaclearing/connect-sdk";

preload({ inviteUrl, embedKey });
```

Pass the same `embedKey` you will pass to `open()`, so the warmed check applies to the launch. Without it the check still runs, but `open()` repeats it with the key.

Runs the iframe check early so the next `open()` chooses its mode with no loading state. It is an optimization, not a requirement. `open()` behaves the same whether or not you call it; the only difference is a loading state about one network round trip long.

It cannot break your page. An invite URL the package cannot derive an origin from is ignored here. The same URL throws from `open()`, where a launch is at stake. Outside a browser, as in a server render, `preload()` does nothing, so it is safe to call from code that also runs on the server.

The result is held for the life of the page and never persisted. Browser settings change between visits, and a remembered "supported" would strand someone in an iframe their browser no longer allows.

## Styling and isolation

The surface the package renders lives in a shadow root. Your global CSS cannot restyle the overlay, and the overlay's CSS cannot leak into your page.

The package's own overlay is a modal dialog. It takes focus when it opens, marks the rest of the document `inert` while it is up, and restores focus and the tab order on teardown. Anything you had already marked `inert` stays that way, so launching from inside your own modal gives your page back the way you had it. Mounted into a `container` you supplied, it does none of this. Your layout, your focus management.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.