Guides
Embed the Connect Widget
Run the OAuth flow in a popup or redirect, from a frontend, with one line of JavaScript.
By the end of this guide, a frontend launches an OAuth flow from a button click. On desktop, the user authorizes, the popup closes, and a callback receives the retained grants plus completion details. The launcher works with React, Vue, Angular, and plain HTML; it uses a popup on desktop and full-page navigation on mobile-classified devices.
The Connect widget is @alter-ai/connect, a framework-agnostic browser package that opens the Alter-hosted Connect UI in a popup or full-page navigation. It uses Zod to validate completion messages before application callbacks consume them.
Prerequisites
Section titled “Prerequisites”- An app with at least one provider configured.
- A backend endpoint that mints Connect session tokens (the app key must stay on the backend — see Call APIs on behalf of users for the backend half).
- A frontend with a
<button>that triggers the connection.
Walkthrough
Section titled “Walkthrough”1. Install
Section titled “1. Install”npm install @alter-ai/connectOr via CDN:
<script src="https://cdn.jsdelivr.net/npm/@alter-ai/connect@latest/dist/alter-connect.umd.js"></script>2. Open the widget on click
Section titled “2. Open the widget on click”import AlterConnect from "@alter-ai/connect";
const alterConnect = AlterConnect.create();
// Fetch before enabling the button. open() must run directly in the click// handler so the popup remains attached to the user gesture.const { sessionToken } = await fetch("/api/connect-session").then(r => r.json());button.disabled = false;
button.addEventListener("click", () => { void alterConnect.open({ token: sessionToken, onSuccess: (grants, completion) => { for (const grant of grants) { console.log("Connected", grant.provider, grant.account_identifier); } if (completion.failedGrants.length > 0) { console.warn("Some providers were not retained", completion.failedGrants); } }, onError: (error) => { console.error("Connect failed", error.code, error.message); }, onExit: () => { console.log("User closed the popup"); }, });});open() resolves after the launch attempt, not after authorization completes. On desktop, completion arrives through onSuccess(grants, completion). grants contains every retained grant; completion.failedGrants reports providers whose grants could not be retained after a partial success.
3. Choose a flow
Section titled “3. Choose a flow”The widget automatically picks the right flow based on device:
| Device | Flow |
|---|---|
| Desktop | Centered popup, 500×700px, posts result via postMessage |
| Phone (≤480px) or tablet portrait | Full-page navigation; returns to the session’s return URL with the outcome |
| Tablet landscape | Popup |
Full-page navigation destroys the JavaScript callbacks passed to that open() call. Set a return URL when creating the session and the flow navigates back to it with the outcome, which the AlterConnect instance constructed on that page load reads and delivers to the onSuccess or onError registered there. A session that authorized several providers returns with the first grant only.
Poll from the application backend when completion must survive navigation regardless — sessions created without a return URL, and returns that arrive more than five minutes after the flow started. Keep the session token on the backend, poll it with app.poll_connect_session() (Python) or app.pollConnectSession() (TypeScript), then expose the resulting status through the application’s own route.
Patterns
Section titled “Patterns”import { useEffect, useState } from "react";import AlterConnect from "@alter-ai/connect";
function ConnectButton() { const [alterConnect] = useState(() => AlterConnect.create()); const [sessionToken, setSessionToken] = useState(null);
useEffect(() => { let isMounted = true; fetch("/api/connect-session") .then(r => r.json()) .then(({ sessionToken }) => { if (isMounted) setSessionToken(sessionToken); }); return () => { isMounted = false; alterConnect.destroy(); }; }, [alterConnect]);
const handleConnect = () => { if (!sessionToken) return; void alterConnect.open({ token: sessionToken, onSuccess: (grants, completion) => { /* … */ }, }); };
return ( <button disabled={!sessionToken} onClick={handleConnect}> Connect Google </button> );}The session is ready before the button is enabled, so open() runs directly in the click handler. Fetching the session inside that handler — with either await or a .then() callback — can detach window.open() from the user gesture and trigger a popup blocker.
<script setup>import { onMounted, ref } from "vue";import AlterConnect from "@alter-ai/connect";const alterConnect = AlterConnect.create();const sessionToken = ref(null);
onMounted(async () => { const response = await fetch("/api/connect-session"); const session = await response.json(); sessionToken.value = session.sessionToken;});
function handleConnect() { if (!sessionToken.value) return; void alterConnect.open({ token: sessionToken.value, onSuccess: (grants, completion) => { /* … */ }, });}</script>
<template> <button :disabled="!sessionToken" @click="handleConnect">Connect Google</button></template>Reauth flow
Section titled “Reauth flow”When a stored grant’s connection breaks (refresh token revoked, user changed password at the provider), the SDK raises CredentialRevokedError on the next API call. Trigger the widget with a fresh session from the same session-creation flow; the resulting OAuth completes as a re-auth and the grant_id stays the same. Each reauthenticated item in the grants array has operation: "reauth" instead of "creation".
Multiple providers in one session
Section titled “Multiple providers in one session”Pass several providers in allowed_providers when minting the session. The widget shows the user a provider picker before launching the OAuth flow:
session = await app.create_connect_session( allowed_providers=["google", "github", "slack"], user_token=user.jwt,)Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
| Popup blocked | The session was fetched inside the click handler, so open() ran after the user-gesture task. | Fetch the session before enabling the button, then call open() directly in the click handler. |
invalid_token | Session token expired (default 10 minutes) or was already used. | Mint a fresh session per click. |
| Mobile completion callback never fires | Full-page navigation destroyed the page’s callback state. | Recover completion by polling the session from the application backend. |
Popup completes but onSuccess never fires | The session was minted without allowed_origin and no website_url is configured on the app. | Either set allowed_origin per session, or set website_url in the app’s portal settings. |
onError fires with popup_blocked | Same as “popup blocked” above. | Same fix. |