Skip to content

Reference

Events & callbacks

Per-open callbacks and the long-lived on / off event bus.

The Connect SDK exposes two parallel APIs for reacting to lifecycle events:

  • Per-open callbacks passed to open(): onSuccess, onError, onExit, onEvent. The three terminal ones fire at most once per open(), and their listeners are removed when the SDK runs close(). onEvent is not terminal and fires as many times as the flow has something to report: connect_opened, one notice per declined or failing provider, and embed_unsupported when the inline widget falls back to a window.
  • The event bus — alterConnect.on(event, handler) / alterConnect.off(event, handler). Listeners stay registered across multiple open() calls.

The two APIs receive the same success, error, and exit transitions. Do not depend on listener ordering: it depends on when each bus listener was registered relative to open(). onEvent additionally receives non-terminal transitions; of those, the ones raised by the hosted flow also reach an event bus listener, while connect_opened reaches onEvent only.

Passed inside the OpenOptions object to open().

onSuccess: (grants: Grant[], completion: ConnectCompletion) => void

Fires when the popup flow finishes with at least one retained grant. The first argument is an array of Grant objects. The second is ConnectCompletion; its failedGrants array reports providers that authorized but were not retained: reason: "not_selected" when the user did not select the provider on the Confirm screen, reason: "activation_failed" when the connection could not be completed as the user confirmed it, any other reason when selected usage limits could not be applied.

onSuccess is the only required callback on open().

await alterConnect.open({
token: sessionToken,
onSuccess: (grants, completion) => {
for (const grant of grants) {
console.log(grant.provider, grant.grant_id);
}
console.log("not retained", completion.failedGrants);
},
});
onError?: (error: AlterError) => void

Fires when a failure reaches the browser SDK — for example, a blocked popup, redirect-state failure, malformed result, server-posted callback error, or total usage-limit application failure. The AlterError shape carries a machine-readable code, a message, optional details, and optional typed failedGrants.

An invalid or expired session detected while the hosted page loads is rendered there instead of being a reliable browser callback. Provider authorization denial fires with code: "connect_denied" only when the session has nothing else left to try; backend polling reports the same terminal decision as 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 and remains pending so the user can continue, reporting the refusal through onEvent instead.

onExit?: () => void

Fires when the user closes the popup window without finishing, or presses Cancel inside the inline widget. Does not fire on the mobile redirect flow — there is no popup to close. Does not fire when the SDK closes the popup or removes the inline widget itself (after success or error). This is a local browser transition: it does not mark the backend session denied, so a server-side poll stays pending until timeout or session expiry.

onEvent?: (
eventName: string,
metadata: Record<string, unknown>,
) => void

Hook for analytics and for flow transitions that are not terminal. The SDK emits four events today:

Event nameFires whenmetadata
connect_openedopen() launched a flow: the popup opened, the redirect started, or the inline widget mounted. It does not fire when the launch was refused (a blocked popup, a failed redirect-state write, or a launch-time rejection); those report through onError only.{ timestamp: string, mode: "popup" | "redirect" | "embed", direct?: true }: an ISO 8601 timestamp and the flow that was launched. direct: true is present when an inline flow opened the provider window from the application’s click instead of rendering a tile. This reports what THIS launch did with the window; it is not the session’s direct consent flag, and an inline launch can carry it while the hosted UI still shows the end user every consent screen.
embed_unsupportedThe inline widget’s browser refused the storage it needs inside a frame, and the widget offered the user a window to finish in instead. Informational: the flow continues and completes through the same callbacks.{ reason: string | null, timestamp: string }
provider_declinedThe user refused authorization on a provider’s consent screen and the session can still continue. metadata.error_code is always connect_denied.{ error_code: string, provider: string | null, error_description: string | null, recoverable: true, timestamp: string }
provider_errorThe provider returned an authorization failure that was not a user refusal — an outage, a revoked or rotated client credential, an unsupported scope — and the session can still continue. metadata.error_code is provider_error: followed by the provider’s own code, for example provider_error:temporarily_unavailable.Same shape as provider_declined.

A session “can still continue” when it still offers a provider the user has not yet declined, when a grant was already authorized during it, or when the user has already connected an account for that provider and can still approve it. In that case the user is returned to the provider picker and the flow stays open. Once the user has declined every offered provider, nothing is left to try and the refusal is terminal (see errors). Note the third condition: a single-provider session is not automatically terminal, so an application that mints one provider per session must still handle these events.

For listeners that need to outlive a single open() call — typically when a long-lived component subscribes once at mount and unsubscribes at unmount — use the bus methods.

alterConnect.on(event: string, handler: (...args: never[]) => void): () => void
alterConnect.off(event: string, handler: (...args: never[]) => void): void

The variadic never[] is the package’s contravariant-safe declaration; concretely typed handlers in the table below are accepted. on() returns an unsubscribe function. Calling it is equivalent to calling off(event, handler).

The event emitter catches and logs exceptions from handlers, then continues to later handlers. The per-open onSuccess, onError, and onExit wrappers perform their final SDK cleanup in a finally, so an exception from application callback code is logged but never leaves the SDK open: isOpen() still returns to false and the widget can still be reopened. onEvent is guarded too: an exception from a connect_opened handler is caught and logged and does not reject open(), and the hosted-flow events (provider_declined, provider_error, embed_unsupported) arrive through the bus, where an exception is caught and logged like any other bus handler.

EventHandler signatureFires when
success(grants: Grant[], completion: ConnectCompletion) => voidThe popup or inline flow retained at least one grant. Same arguments as onSuccess.
error(error: AlterError) => voidA failure delivered to the browser SDK. Same shape as onError.
exit() => voidUser closed the popup without finishing, or pressed Cancel inside the inline widget. Same trigger as onExit.
close() => voidAn open flow was closed, once per open(): by alterConnect.close() directly, or after a per-open success, error, or exit callback, including one that threw. Calling close() with nothing open does not emit it, and destroy() does not emit it.
event(eventName: string, metadata: Record<string, unknown>) => voidA non-terminal transition reported by the hosted flow — provider_declined, provider_error, or embed_unsupported. Does not change flow state or close the UI.

connect_opened is raised directly by open() and does not go through the bus, so it reaches the per-open onEvent callback only. Non-terminal transitions raised by the hosted flow (provider_declined, provider_error, embed_unsupported) go through the event bus, so they reach both a bus listener and onEvent.

import { useEffect, useState } from "react";
import AlterConnect, {
type AlterError,
type ConnectCompletion,
type Grant,
} from "@alter-ai/connect";
const alterConnect = AlterConnect.create();
export function ConnectionStatus() {
const [latest, setLatest] = useState<Grant | null>(null);
useEffect(() => {
const unsubscribeSuccess = alterConnect.on(
"success",
(grants: Grant[], completion: ConnectCompletion) => {
setLatest(grants[0] ?? null);
console.log("not retained", completion.failedGrants);
},
);
const unsubscribeError = alterConnect.on("error", (err: AlterError) => {
console.error("connect failed", err);
});
return () => {
unsubscribeSuccess();
unsubscribeError();
};
}, []);
return latest ? <p>Connected: {latest.provider_name}</p> : null;
}

When to prefer the bus over per-open callbacks

Section titled “When to prefer the bus over per-open callbacks”
  • Cross-component state. Multiple components react to a successful connection — register a single on('success', ...) and dispatch from there.
  • Analytics middleware. A long-lived on('error', ...) handler can forward every failure to an error tracker without each open() site having to remember.

On a full-page redirect the browser leaves the page entirely and comes back to a fresh load, so the instance that called open() no longer exists. The new instance resolves the redirect outcome while it is being constructed — before create() has returned, and therefore before any application code can hold the instance to register a handler.

The outcome is retained for that reason. It is delivered to the first success or error listener registered, whenever that happens:

const alterConnect = AlterConnect.create();
// Registered after create(), and after any awaits — still receives the
// outcome of a redirect that completed before this line ran.
alterConnect.on("success", (grants, completion) => {
console.log("connected", grants);
});

It is delivered once. A later, unrelated on('success', ...) does not receive a repeat of an earlier redirect.

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.