Skip to content

Reference

alterConnect.open()

Launch the Connect UI in a popup, inline in a container element, or a mobile redirect.

open() takes an OAuth Connect session token minted on the application backend and attempts to open the Alter-hosted UI. Desktop popup and inline results are routed through callbacks.

await alterConnect.open({
token: sessionToken,
onSuccess: (grants, completion) => {
console.log("connected", grants);
console.log("not retained", completion.failedGrants);
},
onError: (error) => console.error(error),
onExit: () => console.log("user cancelled"),
onEvent: (eventName, metadata) => console.log(eventName, metadata),
});
alterConnect.open(options: OpenOptions): Promise<void>

The returned promise resolves after the launch attempt, not when the user finishes the flow. A blocked popup still resolves it: onError receives popup_blocked, and connect_opened is not reported because no flow was launched. See OpenOptions for the full type.

NameTypeRequiredDescription
options.tokenstringyesSession token minted by the application backend with app.createConnectSession(). Short-lived (10-minute default).
options.containerHTMLElementnoRender the Connect UI inline inside this element instead of a popup window (see Inline flow). The element must be attached to the document. The widget fills 100% of it; a minimum of 350×300 CSS px is recommended.
options.singleProviderbooleannoInline mode only: the session’s single_provider flag. When open() runs inside the user’s click, the provider’s sign-in window opens immediately (no tile) and the container shows a plain status line; a blocked window falls back to the tile. Ignored without container. This is the session’s PICKER decision, not its consent decision — a session that also opted out of the end-user consent screens carries a separate direct flag, which the widget does not consume.
options.onSuccess(grants: Grant[], completion: ConnectCompletion) => voidyesFired when the popup or inline flow completes with at least one retained grant. The first argument is the grants array. completion.failedGrants reports providers that were not retained: reason: "not_selected" for a provider the user did not select on the Confirm screen, reason: "activation_failed" for one that could not be completed when the user confirmed it, any other reason for usage limits that failed to apply.
options.onError(error: AlterError) => voidnoFired for failures delivered to the browser SDK, such as a blocked popup, redirect-state failure, malformed result, server-posted callback error, or total usage-limit application failure.
options.onExit() => voidnoFired when the user closes the popup without finishing, or presses Cancel inside the inline widget.
options.onEvent(eventName: string, metadata: Record<string, unknown>) => voidnoFired for analytics and for non-terminal flow transitions. See Events & callbacks for the exact events the SDK emits today.

onSuccess is the only required callback. Surfacing onError is strongly recommended, especially for popup-blocked and malformed-result failures.

Because open() is async, these conditions reject its returned promise before a launch attempt:

ConditionError code
The SDK instance was already destroy()-ed.sdk_destroyed
token is missing or not a string.invalid_options
onSuccess is missing or not a function.invalid_options
container was supplied but is not an element attached to the document.invalid_container
container was supplied on a page with no http(s) origin (for example a file:// page).embed_unsupported_origin
container was supplied from a page that is itself inside an iframe. The widget can be framed by exactly one origin and every ancestor must match, so a nested host page can never render it.embed_unsupported_origin

Failures that reach the browser SDK are routed through onError. onSuccess, onError, and onExit run through the event emitter’s exception guard. If one throws, the SDK logs the exception rather than converting it into AlterError, and still performs that callback’s final state/listener cleanup: an exception in application code cannot leave the SDK stuck open. onEvent is guarded the same way: an exception thrown while handling connect_opened or a hosted-flow event is logged and never rejects open().

Invalid or expired tokens detected while binding or loading the hosted page are rendered inside that page. In the popup flow they are not onError callbacks; the inline widget additionally posts them to onError with code: "session_expired" (see Inline flow). A provider authorization denial is terminal only when the session has nothing else left to try: the popup receives code: "connect_denied" through onError and the error event, and backend polling raises ConnectDeniedError. A session that can still continue — an allowed provider the user has not yet declined, a grant already authorized during it, or an account the user has already connected and can still approve — returns to the picker, keeps polling pending, and reports the refusal through onEvent as a non-terminal provider_declined.

On desktop, open() launches a centered 500×700 px popup that loads the Alter Connect UI. The popup communicates back via postMessage; the SDK validates the origin against the configured Alter host before accepting any message.

For a validated success or error message, the flow handler closes the popup before invoking the application callback. Whether that callback returns normally or throws, the SDK then resets its open-state flag, removes per-open listeners, and emits close — so no application-side close() call is needed to recover from a throwing callback.

If the browser blocks the popup, onError fires with code: "popup_blocked". Fetch the session before the click so open() can run directly inside the user gesture.

const token = await fetchToken();
button.disabled = false;
button.addEventListener("click", () => {
void alterConnect.open({ token, onSuccess, onError });
});

Closing the popup without finishing invokes onExit locally. It does not mark the backend session denied, so backend polling remains pending until its own timeout or the session expires.

Pass container to render the Connect UI inside an element on the page instead of a popup window, the way Plaid’s embedded Link does. The widget fills the element, so size the element (350×300 CSS px or larger is recommended).

await alterConnect.open({
token: sessionToken,
container: document.getElementById("alter-connect")!,
onSuccess: (grants, completion) => console.log("connected", grants),
onExit: () => console.log("user cancelled"),
});

Requirements and behaviour:

  • The host page’s origin must equal the session’s allowedOrigin (or the origin of the app’s Website URL when allowedOrigin is omitted). Any other page gets an error rendered inside the widget and onError fires with code: "embed_origin_mismatch"; see Troubleshooting.
  • Provider sign-in cannot run inside a frame. When the user picks a provider (or the widget shows a single provider), the sign-in opens in a new window from that click and the widget updates when it finishes. open() itself therefore does not need to run inside a click handler in inline mode.
  • The widget carries its own Cancel control, which fires onExit. Like closing the popup, Cancel is a local transition: it does not mark the backend session denied, so a server-side poll stays pending until its timeout or the session expires.
  • Mobile devices use the inline widget too; the full-page redirect is never used when container is supplied, and the provider window opens as a new tab.
  • Browsers that refuse the widget’s storage inside a frame (older Safari versions, or third-party cookies disabled) are handled automatically: the widget offers “Continue in a new window” and completes the flow there, still reporting through the same callbacks. onEvent receives embed_unsupported when that happens.
  • close() and destroy() remove the widget from the page.
  • A frame that delivers no message within 15 seconds of mounting (the hosted page was refused by the browser, failed to load, or could not be reached) fires onError with code: "embed_load_timeout".
  • A session the hosted page cannot bind inside the frame renders the error in the widget and posts it to onError with the server’s code: embed_origin_mismatch, embed_not_allowed (the session has no allowed origin the widget can be bound to), session_expired, embed_verify_failed, or cookies_required. A 409 embed_mode_conflict (the session already continued in a new window) is not an error: the widget offers that window again.

onSuccess, onError, and onExit behave exactly as in the popup flow; connect_opened reports mode: "embed".

On the redirect branch, the SDK:

  1. Saves the current page URL and a timestamp in sessionStorage.
  2. Navigates the entire page to the Connect UI URL.
  3. Leaves the original JavaScript context, including per-open callbacks and event listeners.

The returning page load is verified before any callback runs: the SDK mints a per-flow value that is echoed on the return, and callback parameters that do not carry it are ignored, so a hand-crafted link cannot fake a completion. Return state is accepted for at most five minutes and older state is removed silently — past that window, completion is recoverable only by polling the session from the application backend.

A session that authorized several providers returns with the first grant only, because the outcome is reconstructed from a URL. Poll the session to retrieve the full set, including any provider that failed.

For a session that delegated to an agent, the return URL also carries delegated_agent_id, delegable and delegation_policy_refused, which become the grant’s delegation. The SDK removes them from the address bar with the other callback parameters. They belong to the outcome only when delegated_agent_id is present: a session with no agent carries none of them, so an application’s own delegable or delegation_policy_refused query parameter is then left in place. For a session with an agent, the outcome’s values replace the application’s parameters of those names. With delegated_agent_id present, a partial or malformed set (no delegable, an agent id that is not a UUID, or a value that is not true / false) is refused through onError with code: "invalid_response".

onSuccess receives an array of grants because a session can authorize multiple providers before confirmation. When the session retains one provider grant, the array has length 1.

A session minted with exactly one entry in allowedProviders shows no Alter page before the provider, in any flow: there is nothing to pick, so the user goes straight to that provider’s sign-in. The one exception: a user who has already connected that provider sees a short unbranded approval step (Approve the existing account, or Use a different account) instead. The user still confirms the connection on the way back unless the session was also minted with direct, which opts out of the confirmation screen, the duration choice and the policy-authoring step — that is a separate decision from the provider count, made by the application at session creation. Redirect: the page goes to the provider and returns to the session’s return URL with the outcome. Inline: pass the session’s single_provider flag (open({ token, container, singleProvider: session.singleProvider })) from the user’s click and the provider window opens immediately with a plain status line in the container; when the browser blocks the window, a single “Continue with” tile naming that provider renders instead, with no Alter branding on it (the app’s own logo and legal links appear when configured). onSuccess fires exactly as for a multi-provider session, with one grant; a refusal fires onError with connect_denied.

Each provider choice on a rendered Connect screen is single-flight. If a user clicks the same choice repeatedly while navigation starts, overlapping OAuth callbacks resolve to the same grant and the confirmation array contains that grant once. Returning to the picker and choosing again is a new selection.

await alterConnect.open({
token: sessionToken,
onSuccess: (grants, completion) => {
for (const grant of grants) {
console.log(grant.provider, grant.grant_id);
}
for (const failure of completion.failedGrants) {
console.warn(failure.providerId, failure.reason);
}
},
});

Confirm needs at least one selected connection. When the Alter deployment holds new connections until Confirm, a new connection becomes usable only when the user confirms it; on a deployment that does not, it is usable as soon as the provider sign-in completes, without the usage limits or expiry the user had not yet confirmed. A provider the user connected but did not select on the Confirm screen is reported with reason: "not_selected" and is never usable; one that could not be completed when the user confirmed it (for example, the same account was connected again in a newer Connect session) is reported with reason: "activation_failed".

If no authorized grant is kept, onSuccess does not fire and onError receives a typed failedGrants array. The code is grant_policy_application_failed when any grant was rejected because selected usage limits could not be applied, and grant_activation_failed when every selected connection instead could not be completed at Confirm.

See the Grant, ConnectCompletion, and ConnectFailedGrant references.

Recovery is configured when the application backend mints the session. A session returned by createConnectSessionForError() is opened with the same token option as any other Connect session. @alter-ai/connect exports none of the server-side exception classes, including ReAuthRequiredError, NoDelegatedGrantError, GrantNotFoundError, CredentialRevokedError, or the headless-flow ConnectFlowError family (ConnectDeniedError, ConnectConfigError, ConnectTimeoutError).

allowUserPolicyRules defaults to enabled during session creation. It lets the hosted UI add narrowing deny, human-approval, time-window, quota, and operation/parameter content rules. A human-approval rule gates later API calls; Connect authors the rule but does not run the later approval workflow. Session-level grant-expiry bounds separately add a constrained duration picker to the hosted confirmation screen. These are session-creation options, not OpenOptions. Neither adds a step to a session minted with direct, which has no confirmation screen: that session is confirmed with the scopes the application requested and the default duration, and the application’s own grantPolicy bounds still apply. Naming one provider alone does not remove the confirmation screen — the provider count decides the picker, not consent.

The session token passed to open() is minted on the application backend, never in the browser. Provider allowlists, per-provider requested scopes, origin binding, user identity, recovery context, delegated agents, onward-delegation permission, account switching, requested sibling grants, delegation scope constraints, grant-expiry bounds, and user-authored policy rules are all configured there; open() accepts only the seven fields listed on this page.

See Embed the Connect widget and the server SDK references for session creation and polling.

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.