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 peropen(), and their listeners are removed when the SDK runsclose().onEventis not terminal and fires as many times as the flow has something to report:connect_opened, one notice per declined or failing provider, andembed_unsupportedwhen the inline widget falls back to a window. - The event bus —
alterConnect.on(event, handler)/alterConnect.off(event, handler). Listeners stay registered across multipleopen()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.
Per-open callbacks
Section titled “Per-open callbacks”Passed inside the OpenOptions object to open().
onSuccess
Section titled “onSuccess”onSuccess: (grants: Grant[], completion: ConnectCompletion) => voidFires 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
Section titled “onError”onError?: (error: AlterError) => voidFires 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
Section titled “onExit”onExit?: () => voidFires 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
Section titled “onEvent”onEvent?: ( eventName: string, metadata: Record<string, unknown>,) => voidHook for analytics and for flow transitions that are not terminal. The SDK emits four events today:
| Event name | Fires when | metadata |
|---|---|---|
connect_opened | open() 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_unsupported | The 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_declined | The 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_error | The 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.
The event bus: on and off
Section titled “The event bus: on and off”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.
Signatures
Section titled “Signatures”alterConnect.on(event: string, handler: (...args: never[]) => void): () => voidalterConnect.off(event: string, handler: (...args: never[]) => void): voidThe 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.
Events on the bus
Section titled “Events on the bus”| Event | Handler signature | Fires when |
|---|---|---|
success | (grants: Grant[], completion: ConnectCompletion) => void | The popup or inline flow retained at least one grant. Same arguments as onSuccess. |
error | (error: AlterError) => void | A failure delivered to the browser SDK. Same shape as onError. |
exit | () => void | User closed the popup without finishing, or pressed Cancel inside the inline widget. Same trigger as onExit. |
close | () => void | An 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>) => void | A 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.
Example — React component
Section titled “Example — React component”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 eachopen()site having to remember.
Full-page redirects (mobile)
Section titled “Full-page redirects (mobile)”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.