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),});Signature
Section titled “Signature”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.
Parameters
Section titled “Parameters”| Name | Type | Required | Description |
|---|---|---|---|
options.token | string | yes | Session token minted by the application backend with app.createConnectSession(). Short-lived (10-minute default). |
options.container | HTMLElement | no | Render 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.singleProvider | boolean | no | Inline 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) => void | yes | Fired 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) => void | no | Fired 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 | () => void | no | Fired when the user closes the popup without finishing, or presses Cancel inside the inline widget. |
options.onEvent | (eventName: string, metadata: Record<string, unknown>) => void | no | Fired 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.
Launch-time rejections
Section titled “Launch-time rejections”Because open() is async, these conditions reject its returned promise before a launch attempt:
| Condition | Error 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.
Desktop popup flow
Section titled “Desktop popup flow”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.
Inline (embedded) flow
Section titled “Inline (embedded) flow”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 whenallowedOriginis omitted). Any other page gets an error rendered inside the widget andonErrorfires withcode: "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
containeris 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.
onEventreceivesembed_unsupportedwhen that happens. close()anddestroy()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
onErrorwithcode: "embed_load_timeout". - A session the hosted page cannot bind inside the frame renders the error in the widget and posts it to
onErrorwith 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, orcookies_required. A409 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".
Mobile redirect flow
Section titled “Mobile redirect flow”On the redirect branch, the SDK:
- Saves the current page URL and a timestamp in
sessionStorage. - Navigates the entire page to the Connect UI URL.
- 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".
Multiple providers in one session
Section titled “Multiple providers in one session”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 and approval rules
Section titled “Recovery and approval rules”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.
Backend session creation
Section titled “Backend session creation”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.