Alter Connect SDK
A lightweight JavaScript SDK for embedding OAuth integrations, and end users' own API keys for managed-secret providers, into an application. The SDK opens Alter Connect in a desktop popup, uses full-page navigation on mobile-classified devices, or renders inline inside an element the application provides, and validates completion data before invoking typed callbacks.
Typed OAuth callbacks | Zod-validated payloads | TypeScript included
Quick Start
1. Install
npm install @alter-ai/connect
The package metadata declares Node.js 20.19 or later for npm tooling. Browser execution does not require Node.js. Or use the UMD bundle via CDN:
<script src="https://cdn.jsdelivr.net/npm/@alter-ai/connect@latest/dist/alter-connect.umd.js"></script>
2. Get a Session Token from the Application Backend
The application backend creates a short-lived session token using the Alter SDK:
// Application backend (Node.js example using @alter-ai/alter-sdk)
import { App, CallerType } from "@alter-ai/alter-sdk";
const alterApp = new App({
apiKey: process.env.ALTER_API_KEY!,
caller: "my-backend",
callerType: CallerType.SERVICE,
});
const session = await alterApp.createConnectSession({
allowedProviders: ["provider-id"],
allowedOrigin: "https://app.example.com",
// Optional. When the user approves access, the Connect UI also lets them set
// their own usage limits on the connection being created — deny rules,
// human approval, time windows, request quotas, and operation/parameter
// rules. They can only narrow what they are granting, never widen it, and
// they can change or remove the limits later in their wallet.
//
// This step is enabled by default. To hide it, pass:
// allowUserPolicyRules: false,
});
const sessionToken = session.sessionToken;
3. Open the Connect UI
import AlterConnect from '@alter-ai/connect';
// Initialize SDK (no API key needed!)
const alterConnect = AlterConnect.create();
const connectButton = document.querySelector('#connect-button');
if (!(connectButton instanceof HTMLButtonElement)) {
throw new Error('Connect button is missing');
}
connectButton.disabled = true;
// Prefetch before the user interacts. Browsers may block a popup opened only
// after an awaited network request has detached it from the click gesture.
let sessionToken = null;
fetch('/api/alter/session')
.then(response => response.json())
.then(({ session_token }) => {
sessionToken = session_token;
connectButton.disabled = false;
})
.catch(() => {
console.error('Unable to prepare the Connect session');
});
connectButton.addEventListener('click', () => {
if (!sessionToken) return;
void alterConnect.open({
token: sessionToken,
onSuccess: (grants, completion) => {
console.log('Connected!', grants);
grants.forEach(grant => console.log(grant.provider, grant.grant_id));
completion.failedGrants.forEach(failure => {
console.warn(failure.providerId, failure.reason, failure.message);
});
},
onError: (error) => {
console.error('Failed:', error);
},
onExit: () => {
console.log('User closed the window');
}
});
});
The package launches the hosted OAuth Connect UI and validates desktop popup results before callbacks receive them.
Framework Examples
React, Vue, Angular, Svelte, and vanilla applications use the same pattern:
- Create one reusable
AlterConnectinstance. - Fetch a session token before enabling the Connect button.
- Call
open()directly in the click handler so popup activation remains attached to the user gesture. - Destroy the instance when the owning component unmounts.
The CDN bundle exposes the same class as window.AlterConnect. CommonJS
consumers read the default export with
require('@alter-ai/connect').default.
API Reference
AlterConnect.create(config?)
Creates a new SDK instance.
const alterConnect = AlterConnect.create({
debug: true // Enable console logging (default: false)
});
| Option | Type | Description | Default |
|---|---|---|---|
debug | boolean | Enable debug logging | false |
baseURL | string | Reserved and unsupported. Passing any value throws. | — |
Note: Visual customization (colors, fonts, logo) is configured via the Developer Portal branding settings. Alter Connect applies the configured branding automatically.
alterConnect.open(options)
Attempts to open Connect. Desktop uses a centered popup window (500×700 px). Mobile-classified devices use full-page navigation, with the limitation documented under Mobile behavior. Supplying container renders Connect inline inside that element on every device instead (see Inline mode).
await alterConnect.open({
token: 'sess_abc123...',
onSuccess: (grants, completion) => { /* ... */ },
onError: (error) => { /* ... */ },
onExit: () => { /* ... */ },
onEvent: (eventName, metadata) => { /* ... */ }
});
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Short-lived session token created by the application backend with an Alter SDK: sess_… for OAuth Connect or ms_sess_… for a managed-secret session |
container | HTMLElement | No | Render Connect inline inside this attached element instead of a popup window. The widget fills the element (350×300 px minimum recommended). The host page's origin must equal the session's allowed origin. |
singleProvider | boolean | No | Inline OAuth mode only (ignored for a managed-secret session): the session's single_provider flag (true when the session names exactly one provider). Called from the user's click, the provider window opens immediately with a plain status line in the container; a blocked window falls back to the inline tile. This is the picker decision, not the consent decision — the session's separate direct flag governs the hosted consent screens and the widget does not consume it. |
onSuccess | (grants, completion) => void | Yes | Called with the legacy grants array and a typed completion object. completion.failedGrants identifies partial failures. |
onError | (error) => void | No | Called for failures delivered to the browser SDK, including a terminal provider denial (code: "connect_denied"). |
onExit | function | No | Called when the user closes the desktop popup or presses Cancel inside the inline widget; this does not mark the backend session denied |
onEvent | (eventName, metadata) => void | No | Called with connect_opened once a flow launches (metadata.mode names the flow: popup, redirect, or embed; a blocked popup reports popup_blocked through onError and no connect_opened), with provider_declined when the user refuses a provider in a session that can still continue, with provider_error when the provider itself fails in such a session, with embed_unsupported when the inline widget had to fall back to a window, and with embed_continue when an inline managed-secret flow continues in a window the hosted page opened |
The promise resolves after the launch attempt, not after user authorization. Missing or invalid token or onSuccess values, including a token that starts with neither sess_ nor ms_sess_, reject the returned promise with code: "invalid_options"; a container that is not an element attached to the document rejects with code: "invalid_container", and a host page with no http(s) origin, or one that is itself inside an iframe, rejects with code: "embed_unsupported_origin". baseURL is not an OpenOptions field: TypeScript rejects it, while plain JavaScript ignores it.
Inline (embedded) mode
await alterConnect.open({
token: 'sess_abc123...',
container: document.getElementById('alter-connect'),
onSuccess: (grants, completion) => { /* ... */ },
onExit: () => { /* user pressed Cancel inside the widget */ },
});
The hosted Connect UI renders inside container, which the application sizes. Provider sign-in cannot run inside a frame, so the widget opens it in a new window: from the application's own click when a single-provider session's single_provider flag is passed as singleProvider (no tile), otherwise from the user's click on the tile inside the widget; the widget updates when the window finishes; open() itself need not run inside a click handler in this mode. The widget carries its own Cancel control (onExit). Mobile devices use the inline widget too (never the full-page redirect). When the session's single_provider flag is not passed as singleProvider, or the browser blocked the window it tried to open, a session created for exactly one provider shows a single "Continue with" tile naming that provider. Either way the user confirms the connection after the provider returns, unless the session was also minted with direct, which closes on delivery instead. Browsers that refuse the widget's storage inside a frame are handled automatically: the widget offers "Continue in a new window" and completes there through the same callbacks (onEvent receives embed_unsupported).
onSuccess, onError, and onExit run through the event emitter's exception
guard. If one throws, the SDK logs the exception and still performs that
callback's final state/listener cleanup. onEvent splits: connect_opened is
invoked directly, so an exception from it rejects open() after the launch
attempt, while the hosted-flow events run through that same guard. The terminal
handlers are always registered and always release the flow, so a manual popup
close resets isOpen() whether or not onExit was supplied and whether or not
it threw.
Completion data (onSuccess):
For compatibility, the first argument remains an array of Grant objects. The
second argument makes partial completion explicit:
interface ConnectCompletion {
grants: Grant[];
failedGrants: Array<{
providerId: string;
reason: string;
message: string;
}>;
}
failedGrants is empty for full success. When at least one provider succeeds,
it identifies any providers that were connected but not kept: not_selected
(the user did not tick it on the Confirm screen), activation_failed (it could
not be completed when the user confirmed), or another code when the selected
usage limits could not be applied. Unknown reason strings are preserved.
Error Object (onError):
{
code: string;
message: string;
details?: Record<string, unknown>;
failedGrants?: Array<{
providerId: string;
reason: string;
message: string;
}>;
}
A completion with no successful grants and one or more failed grants invokes
onError instead of reporting bare success. The code is
grant_policy_application_failed when any grant was rejected because the
selected usage limits could not be applied, and grant_activation_failed when
every selected connection instead could not be completed at Confirm (start the
connection again).
A provider denial is terminal only when the session has nothing left to try.
Desktop delivers code: "connect_denied" to onError and the error event; a
mobile flow with returnUrl delivers the same code to the returning page's
error listener. If the session still offers a provider the user has not yet
declined, already authorized a grant, or still has an account the user has
connected and can approve, Connect
returns to the picker and leaves backend polling pending so the user can
continue — and reports the refusal as a non-terminal provider_declined event
(onEvent and the event bus) rather than an error, because onError closes
the Connect UI and would strand a user who declined only one of several
providers. A provider failure that was not a refusal reports as provider_error
on that same channel, so an application must branch on the event name rather
than treat every notice as a user decision. Both are popup-flow only: the mobile
returnUrl flow has no opening window to notify. Closing the popup through
browser chrome remains onExit; it is not a provider denial.
alterConnect.close()
Closes an active popup (or removes the inline widget) if one exists, stops the
current flow handler, removes the current per-open callbacks, resets
isOpen(), and emits close.
alterConnect.close();
alterConnect.destroy()
Destroys the SDK instance and cleans up resources.
alterConnect.destroy();
alterConnect.on(event, handler)
Register an event listener. Returns an unsubscribe function.
const unsubscribe = alterConnect.on('success', (grants, completion) => {
console.log('Connected:', grants);
console.log('Providers not kept:', completion.failedGrants);
});
// Later: unsubscribe();
Emitted bus events: success, error, exit, close, event. The success handler receives (grants, completion); the event handler receives (eventName, metadata) and carries the non-terminal transitions provider_declined, provider_error, embed_unsupported and embed_continue. connect_opened is raised directly by open() and reaches the per-open onEvent callback only.
alterConnect.off(event, handler)
Removes a previously registered event handler. Pass the same handler reference
that was supplied to on().
const handleError = error => console.error(error);
alterConnect.on('error', handleError);
alterConnect.off('error', handleError);
alterConnect.isOpen()
Returns the SDK's open-state flag. It becomes true when a launch starts and
returns to false when close() runs. A manually closed popup resets it
automatically, whether or not onExit was supplied and whether or not that
callback threw.
if (alterConnect.isOpen()) {
console.log('Connect is open');
}
alterConnect.getVersion()
Gets the SDK version.
console.log(alterConnect.getVersion()); // the installed package version
Returns the published package version.
Mobile behavior
Without a container, the SDK chooses the flow using user-agent, touch, viewport, and orientation checks:
| Device | Flow | How It Works |
|---|---|---|
| Desktop | Popup | Opens a centered popup (500x700px) and reports completion through callbacks |
| Mobile-classified viewport ≤480 px | Redirect | Navigates the full page to Connect |
| Mobile-classified viewport 481–1024 px in portrait | Redirect | Navigates the full page to Connect |
| Other devices | Popup | Uses the desktop callback flow |
Any device, container supplied | Inline | Renders the hosted UI inside the element; provider sign-in opens in a new window (a new tab on phones) and the widget reports completion through callbacks |
Redirect completion: full-page navigation destroys the callbacks and listeners registered by the original page. Set returnUrl on the session and Connect navigates back to it with the outcome, which is delivered to the callbacks registered on the instance constructed by the returning page. Callback parameters that do not carry the per-flow value the SDK minted are ignored. Sessions created without a returnUrl, and returns arriving more than five minutes after the flow started, complete through createConnectSession() plus pollConnectSession() on the application backend.
Closing the desktop popup invokes onExit locally, shortly after the window
closes (an outcome the window sent just before closing is delivered instead),
but sends no denial transition to the backend. A server-side poll remains pending until its timeout
or the session expires.
A provider can likewise render an authorization error on its own origin without
redirecting to Alter. The browser SDK cannot observe that page or manufacture a
provider error. Server SDK polling reports the no-callback outcome as
ConnectTimeoutError; details.reason distinguishes a local polling deadline
from a session that was first observed pending and later expired.
Security
- API keys stay on the backend. The frontend only receives a short-lived session token minted by the application backend.
- No API keys or provider credentials in the browser. The frontend receives only the short-lived Connect session token.
- The inline widget is isolated from the host page. It renders on Alter's origin inside a frame the host page cannot read, accepts only the session's allowed origin as its parent, and only hands the outcome to that origin. Provider sign-in always runs in a top-level window.
Provider selection
The hosted UI renders the providers permitted by the session. When that provider is not already connected, a one-provider allowlist still renders one available provider. A verified user with an existing healthy connection can use the consent fast path; requestedGrant, when present, controls the sibling grant requested on that connection. Set switchAccount: true while creating the session to force full provider authorization.
Session-controlled behavior
Provider allowlists, per-provider requested scopes, origin binding, user identity, requested sibling grants, delegated agents, onward-delegation permission, delegation scope constraints, account switching, grant-expiry bounds, and policy authoring are session-creation concerns. They are not browser OpenOptions. Grant-expiry bounds add a duration picker to the hosted confirmation screen.
A recovery session from the server SDKs' createConnectSessionForError() is
opened like any other session by passing its sessionToken to open(). This
browser package exports none of the server-side exception classes, including
ReAuthRequiredError, NoDelegatedGrantError, GrantNotFoundError,
CredentialRevokedError, or the headless-flow ConnectFlowError family
(ConnectDeniedError, ConnectConfigError, ConnectTimeoutError).
With allowUserPolicyRules enabled, the hosted UI can author narrowing deny,
human-approval, time-window, quota, and operation/parameter content rules.
Human approval occurs on later API calls; Connect only authors the rule. If
selected rules fail to apply for some providers,
completion.failedGrants reports the partial failure. If no grant remains,
onError receives grant_policy_application_failed.
Managed-secret sessions
A managed-secret Connect session lets the end user enter their own API key for a managed-secret provider, or approve the use of a grant they already hold, and optionally delegates that access to an agent. The application backend creates it with an Alter server SDK and hands its sessionToken (it starts with ms_sess_) to open(), exactly as for OAuth. The widget opens the managed-secret page for an ms_sess_ token and the OAuth page for a sess_ token, and rejects any other token with invalid_options.
// Application backend
const session = await alterApp.createManagedSecretConnectSession({
provider: "<provider-slug>", // the provider's slug, not its template id
userToken: "<user-jwt>",
allowedOrigin: "https://app.example.com",
agent: "<agent-id-or-name>", // optional: omit for the application's own use
});
// Browser
await alterConnect.open({
token: session.sessionToken,
onSuccess: (grants, completion) => {
if (completion.kind !== "managed_secret") return;
const { path, credentialKind, preflight } = completion.managedSecret;
// grants[0].grant_id is the grant to use: the agent's delegation when an
// agent was named (also grants[0].delegation?.grant_id), else the user's grant.
console.log(path, credentialKind, preflight?.status, grants[0].grant_id);
},
onExit: () => {
// The user cancelled (popup or inline).
},
onError: (error) => {
// "access_denied": the user cancelled on a redirect return. Any other code:
// the request ended before it was answered (for example "session_expired"
// or "key_attempts_exhausted"); create a new session.
},
});
completion.kind is "managed_secret" for an ms_sess_ token and "oauth" for a sess_ token; the widget sets it from the token, never from the hosted page. completion.managedSecret reports what the user confirmed:
| Field | Values |
|---|---|
path | key_entry (the user entered their own key; the application can revoke that grant, never edit it), repair (the user replaced their key in place: a session created with repairGrantId, or one whose user's own key had expired), fast_path (the user approved a grant they already held) |
credentialKind | end_user (the user's own key) or developer (a key an administrator set) |
preflight | The provider's check of a key the user entered: { status, message } with status passed, inconclusive (stored without being verified; message says why) or skipped (the provider cannot be checked). null when no key was entered. |
grants holds one grant: provider is the provider's slug and scopes is empty. With an agent, grants[0].delegation.grant_id is the agent's delegation, the grant id the agent passes. failedGrants is always empty. A managed-secret success without a valid outcome rejects with invalid_response.
Popup, redirect and inline (container) modes all work. On a mobile-classified device the page navigates back to the session's returnUrl, and the instance constructed by the returning page delivers the same completion. Inline, the hosted page opens its own window for sign-in and key entry (reported to onEvent as embed_continue) and hands the outcome back through the frame. singleProvider is ignored for a managed-secret session.
TypeScript Support
Full TypeScript definitions included:
import AlterConnect, {
type AlterConnectConfig,
type OpenOptions,
type ConnectCompletion,
type ConnectFailedGrant,
type ManagedSecretConnectCompletion,
type ManagedSecretOutcome,
type Provider,
type Grant,
type AlterError
} from '@alter-ai/connect';
const alterConnect = AlterConnect.create({ debug: true });
await alterConnect.open({
token: sessionToken,
onSuccess: (grants: Grant[], completion: ConnectCompletion) => {
for (const grant of grants) {
console.log(grant.grant_id); // Store this in the application database
console.log(grant.provider);
console.log(grant.scopes);
}
for (const failure of completion.failedGrants) {
console.warn(failure.providerId, failure.message);
}
},
onError: (error: AlterError) => {
console.error(error.code, error.message);
}
});
The package has one runtime dependency: Zod 4 validates complete cross-window result payloads before callback data is consumed.
Browser Support
The CJS, ESM, and UMD bundles target ES2020 and use modern browser APIs including popup or full-page navigation, postMessage, URL parsing, and sessionStorage. Importing the package is side-effect free, but AlterConnect.create() must run in a browser because it checks redirect state during construction. The repository's product E2E suite exercises Chromium; it does not establish a cross-browser version matrix. The package metadata declares Node.js 20.19 or later for npm tooling; CDN browser use does not require Node.js.
Troubleshooting
Popup Blocked
Problem: Browser blocks the OAuth popup
Solution: Ensure alterConnect.open() is called directly from a user interaction (click event):
// May be blocked: the network request runs after the click.
button.addEventListener('click', async () => {
const token = await fetchToken();
void alterConnect.open({ token, onSuccess, onError });
});
// Reliable: fetch before enabling the button.
const token = await fetchToken();
button.disabled = false;
button.addEventListener('click', () => {
void alterConnect.open({ token, onSuccess, onError });
});
Inline Widget Shows An Error
Problem: open({ container }) mounts the widget, it renders an error, and onError fires with code: "embed_origin_mismatch"
Solution: The host page's origin must equal the session's allowed origin (allowedOrigin at session creation, or the origin of the app's Website URL when it is omitted). Pass the exact origin of the page that embeds the widget.
Inline Widget Never Loads
Problem: open({ container }) mounts the widget, it stays empty, and onError fires with code: "embed_load_timeout" after 15 seconds
Solution: The frame delivered no message: the browser refused the hosted page, it failed to load, or the Alter host was unreachable. Confirm the page is served over http(s) at the session's allowed origin and is not itself inside an iframe (a nested host page rejects open() with code: "embed_unsupported_origin" up front), then mint a fresh session and retry.
Provider Window Did Not Open (Inline Mode)
Problem: Clicking a provider inside the inline widget does nothing
Solution: The widget opens the provider sign-in in a new window from that click; if the browser blocks it, the widget shows a message asking to allow pop-ups for the site and the click can be retried.
Session Token Expired
Problem: The hosted Connect page reports that the session expired or is invalid
Solution: Session tokens have a 10-minute default lifetime. Create a new session and open its token. An expiry detected while binding or loading the hosted page is rendered there; it is not a reliable browser onError callback.
CORS Errors
Problem: Session creation was attempted from browser code
Solution: Create sessions on the application backend with an Alter server SDK, then return only the short-lived session token to the browser.
Support
- Documentation: https://docs.alterauth.com
- Issues: GitHub Issues
- Email: [email protected]
License
MIT