> ## 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.

# Without the package

> Embed Catena Connect with a plain iframe, link or window, and read the flow's completion messages yourself

Everything the package does goes through a URL and a message channel, so a page can do it by hand. You set a few parameters on the invitation URL, open it in a frame, a window or the current tab, and listen for the messages the flow posts as it goes.

What the package adds on top:

* the cookie probe, and the fallback to a window or a redirect
* the one-launch-at-a-time guard
* [`resume()`](/get-started/connect-sdk/javascript-reference#resume-options)

[What a plain integration gives up](#what-a-plain-integration-gives-up) covers what you handle yourself without them.

## The launch URL

| Parameter | Value | `open()` option | Note |
| - | - | - | - |
| `embed_key` | your embed key | `embedKey` | required |
| `embed` | `iframe`, `popup` or `redirect` | none; the package picks it | required; you pick it here, and it must match how you open the URL |
| `variant` | `full` or `card` | `variant` | optional |
| `theme` | `light` or `dark` | `theme` | optional |
| `brand_color` | six hex digits | `brandColor` | optional; needs a resolving key; a leading `#` is accepted but must be written `%23` in a hand-written URL |
| `brand_secondary` | six hex digits | `brandSecondary` | optional; needs a resolving key; written like `brand_color` |
| `brand_logo` | an `https` URL | `logoUrl` | optional; needs a resolving key |
| `brand_logo_dark` | an `https` URL | `logoUrlDark` | optional; needs a resolving key |

`embed` names the context the flow is opened in: `iframe` for a frame on your page, `popup` for a window you open, `redirect` for the current tab.

<Warning>
  The flow picks its session cookie attributes from `embed`, so a value that does not match the context is not cosmetic. A mismatched value breaks the session cookie or the message channel. A frame sent `popup` or `redirect` gets a cookie the browser may not send back from a frame, so the flow can fail partway. A window sent `iframe` posts no messages.
</Warning>

Set the parameters on the invitation URL, which keeps whatever it already carries. Invitation URLs already have a query string (`?invite=…`, or `?tms_invite=…` for a TMS invitation), so add the parameters to it rather than starting a second one. In script, `URL.searchParams` does that for you, and it also encodes a leading `#` in `brand_color` and `brand_secondary`:

```js theme={null}
function launchUrl(inviteUrl, embed) {
  const url = new URL(inviteUrl);
  url.searchParams.set("embed_key", "YOUR_EMBED_KEY");
  url.searchParams.set("embed", embed);
  return url.toString();
}
```

The URL must be `https`, except on `localhost`, `127.0.0.1` and `[::1]`, for the reasons the [`inviteUrl` option](/get-started/connect-sdk/javascript-reference#open-options) gives. The flow ignores parameters it does not recognize.

The [Presentation options](/get-started/connect-sdk/javascript-reference#presentation-options) section describes what the six optional parameters do, and the same rules hold here:

* The flow validates the values.
* An invalid value falls back to its default on its own.
* The four branding parameters apply only when the embed key resolves.

## A plain iframe, link or window

The HTML examples use `https://connect.example.com/?invite=YOUR_INVITE_ID` where your invitation URL goes, with the parameters joined to its query string by `&`. The script examples use `inviteUrl` and the `launchUrl()` helper from [The launch URL](#the-launch-url).

### A frame

A frame on your page, with `embed=iframe`. It needs no `allow` attribute, because the flow uses nothing a cross-origin frame is denied by default:

```html theme={null}
<iframe
  src="https://connect.example.com/?invite=YOUR_INVITE_ID&embed_key=YOUR_EMBED_KEY&embed=iframe"
  title="Catena Connect"
></iframe>
```

Do not add `sandbox`. The flow opens its own window for some providers' sign-in, and a sandbox without `allow-popups` blocks it.

### A link

A link, or a navigation, with `embed=redirect`. The flow runs in the tab. If your invitation has a success and a failure URL, the flow returns to one of them, and that is where you learn the outcome. Without them, the flow ends on its last screen and your webhooks are the record. See [The redirect path](/get-started/connect-sdk/how-it-works#the-redirect-path).

```html theme={null}
<a href="https://connect.example.com/?invite=YOUR_INVITE_ID&embed_key=YOUR_EMBED_KEY&embed=redirect">
  Connect your fleet
</a>
```

### A window

A window, with `embed=popup`, opened under the name `catena-connect` and without `noopener`. `noopener` would sever the channel the messages travel on.

Reusing the name `catena-connect` lands a second `window.open()` on the first window, but reloads it and restarts the flow. To bring an open window back, check `popup.closed` and call `popup.focus()` instead.

```js theme={null}
let popup = null;

button.addEventListener("click", () => {
  // A second click brings the open window back instead of restarting the flow.
  if (popup !== null && !popup.closed) {
    popup.focus();
    return;
  }
  popup = window.open(launchUrl(inviteUrl, "popup"), "catena-connect");
  if (popup === null || popup.closed) {
    window.location.assign(launchUrl(inviteUrl, "redirect"));
  }
});
```

Call `window.open()` synchronously inside the click handler. An `await` between the click and the call spends the activation, and the browser blocks the window. `window.open()` returns `null` when the browser refused, and some blockers return a window that is already closed. The package treats both as blocked and navigates the tab with `embed=redirect`, and the example does the same.

<Note>
  With no listener at all, your [connection webhooks](/api-reference/webhooks/overview) tell you the outcome, and on the redirect path so does the page the flow returns to, if your invitation has redirect URLs. For many sites that is a complete integration.
</Note>

## Reading the completion messages

A frame or a window posts a message to the page that opened it when the flow is ready, as each provider is connected or a connection is deleted, and when the user finishes. Every message has this shape, with the detail beside `event` and no wrapper around it:

```ts theme={null}
type CatenaConnectMessage = {
  source: "catena-connect";
  version: 1;
  event:
    | "open"
    | "connection"
    | "connection_deleted"
    | "success"
    | "exit"
    | "close";
  connectionId?: string;    // with "connection" and "connection_deleted"
  connectionIds?: string[]; // with "success"
  reason?: string;          // with "exit"
};
```

The events mean what the [callbacks](/get-started/connect-sdk/events) of the same name mean, including the two `exit` reasons for an [invite link that never loaded](/get-started/connect-sdk/events#onexit-when-the-invite-link-never-loaded). The flow posts `close` straight after `success` or `exit`, never on its own. A user who closes your window produces nothing. If the surface stays up the user can press the button again, and the outcome and `close` are posted again. The package keeps only the first outcome, so a plain listener should too.

`open` can arrive more than once, for example after the embedded page reloads, so make its handler safe to run twice.

Check three things before you trust a message, on every message:

1. `event.origin` is the connect origin, the origin of your invitation URL.
2. `event.source` is the frame or window you opened.
3. `event.data.source` is `"catena-connect"`.

A listener that does exactly that:

```js theme={null}
const connectOrigin = new URL(inviteUrl).origin;
const frame = document.querySelector("iframe");
// The outcome can be posted more than once. Act on the first one only.
let settled = false;

window.addEventListener("message", (event) => {
  if (event.origin !== connectOrigin) return;
  if (event.source !== frame.contentWindow) return;
  if (event.data?.source !== "catena-connect") return;

  switch (event.data.event) {
    case "open":
      flowIsReady();
      break;
    case "connection":
      noteConnection(event.data.connectionId);
      break;
    case "connection_deleted":
      forgetConnection(event.data.connectionId);
      break;
    case "success":
      if (settled) break;
      settled = true;
      continueYourFlow(event.data.connectionIds);
      break;
    case "exit":
      if (settled) break;
      settled = true;
      offerRetry(event.data.reason);
      break;
    case "close":
      frame.remove();
      break;
  }
});
```

For a window, compare `event.source` with what `window.open()` returned.

The listener does not check `version`, for the same reason the package does not: it handles the events it recognizes in any version, so a version bump cannot silence it.

`open` is the flow's own message and reaches a registered origin only, so receiving it is also the sign your origin is registered, as it is for [`onOpen`](/get-started/connect-sdk/overview#quick-start) with the package.

Ignore events, versions and fields you do not recognize. Additions arrive in minor releases, and [Versioning and support](/get-started/connect-sdk/versioning#what-the-promise-covers) says what is promised to stay.

Which events arrive depends on how the URL was opened:

| Opened as | Events delivered |
| - | - |
| Frame | All six. |
| Window | All six, with one gap: a user who closes the window posts nothing. The package polls the window's `closed` property every half second to produce `onClose` there. A plain integration that needs to know does the same. |
| Redirect | None. The tab has navigated away, and the redirect URL it returns to, if the invitation has them, is the signal. |

## What a plain integration gives up

* **No cookie probe and no fallback.** The package checks whether the browser will send the flow's cookie in a frame, and falls back to a window or a redirect when it will not. A plain frame in a browser that refuses third-party cookies shows the flow failing to sign in instead. If you cannot vouch for the browsers, open a window with `embed=popup` or navigate the tab with `embed=redirect`.
* **No single-launch guard.** Two frames run two flows. Opening a window under the name `catena-connect` each time lands a second `window.open()` on the first window, but reloads it and restarts the flow.
* **No de-duplication of the outcome.** The outcome and `close` can be posted more than once. Keep the first outcome yourself.
* **No teardown of the window.** The package closes it on `destroy()`. You close your own.
* **No `resume()`.** Which destination page was reached says how the flow ended, and your webhooks carry the detail, as they do on the package's redirect path. Without a record written before the navigation, though, a destination page cannot tell a return from the flow apart from a direct visit.
* **No shadow-root surface.** You style and position the frame or the window, and you decide when to take it down.

Frame when you control the browsers, window when you do not, redirect when you need nothing on the page at all. The [three modes](/get-started/connect-sdk/how-it-works#the-three-modes) the package moves between are all available to a plain integration, by choosing the `embed` value and the opening method yourself.


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