Each user's account
It's common to want the agent to act as the person it's talking to. For instance, "Which Linear issues are assigned to me?" only works with that person's own Linear.
Let each user connect
Add a connector by its name:
name: assistant # an internal assistant for your team
mode: text
llm: llm-fast
connectors:
- linear # each user signs in to their own LinearThe user signs in at Linear, and the agent never sees their password.
A test in the terminal has no signed-in user, so it leaves linear out and tells you so. Test it in the Stream Dashboard, where you are the signed-in user:
curl -fsSL https://getstream.io/cli/beta/install.sh | bash # the agents commands are in the beta CLI
getstream agents test assistant --sync --dashboardAnd ask: "Which Linear issues are assigned to me?". The reply shows a Connect Linear button. Sign in, and the same reply goes on to list your issues.
Connect in your app's chat
In your own app, the reply asks the same way: the call's step waits for the user to sign in, and the AI components show the Connect Linear card on it. Give each session a returnUrl on your page's origin, where the sign-in comes back, and an approver that runs it:
const returnUrl = `${window.location.origin}/connected.html`;
const session = await api.agent("assistant").sessions.create({ returnUrl });
const approver: AIToolApprover = {
userId: "alice",
clientId: AIClientIdentity.installId(),
decide: async (call, allowed) => session.decide(call.id, { allowed }),
authorizer: createPopupAuthorizer({
returnUrl,
decline: async (call) => session.declineLogin(call.id),
}),
};
<AIConversation approver={approver} messages={messages} />;The return page reports how the sign-in went and closes the popup:
<script type="module">
import { completeConnectorSignIn } from "stream-chat-react/ai-components";
completeConnectorSignIn();
</script>Connect opens the sign-in in a popup, and the same reply goes on once the user is in. Not now calls session.declineLogin, so the call runs nothing and the agent says the user didn't connect. On iOS and Android, name your app's own scheme in reverse domain form, such as com.example.assistant:/connected, as the session's returnURL or returnUrl, and the sign-in card opens the page in the system's sign-in sheet. The React, Swift and Android assistant samples do this.
A sign-in comes back only to the return_url the session was created with, so a conversation started on a phone finishes its sign-ins on that phone.
Connect from your app
Your app can also show its own Connect button. Your backend starts the sign-in, your frontend opens it in a popup, and the popup comes back to your page:
sequenceDiagram
participant B as Your backend
participant F as Your frontend
participant R as Stream router
actor U as User
participant P as Provider
B->>R: POST /v1/agents/connections
R-->>B: id, status pending
B->>R: POST /v1/agents/connections/{id}/authorizations
R-->>B: launch_url, handoff_token
B-->>F: launch_url, handoff_token
F->>R: open launch_url in a popup
R-->>F: va.connector.oauth.ready
F->>R: va.connector.oauth.handoff with handoff_token
R->>P: the popup goes to the provider
U->>P: signs in and approves
P->>R: the popup comes back to the router
R-->>F: the popup goes to return_url?connection_id=...&status=connectedYour backend: create the connection
$STREAM_ACCELERATION_URL is your router's URL, and $SERVER_TOKEN is a server token: an HS256 JWT of {"server": true} signed with your API secret. X-Stream-User-Id and owner.user_id name the same user:
curl -X POST "$STREAM_ACCELERATION_URL/v1/agents/connections" \
-H "api_key: $STREAM_API_KEY" -H "stream-auth-type: jwt" \
-H "Authorization: Bearer $SERVER_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Stream-User-Id: alice" \
-d '{"connector_id": "linear", "owner": {"type": "user", "user_id": "alice"}}'It returns the connection, pending, with its id. Keep the id with your user: you need it to check the status and to reconnect.
Your backend: start the sign-in
Use that id as $CONNECTION_ID:
curl -X POST "$STREAM_ACCELERATION_URL/v1/agents/connections/$CONNECTION_ID/authorizations" \
-H "api_key: $STREAM_API_KEY" -H "stream-auth-type: jwt" \
-H "Authorization: Bearer $SERVER_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Stream-User-Id: alice" \
-d '{"return_url": "https://app.example.com/settings/integrations"}'It returns a launch_url and a handoff_token for your frontend. The sign-in expires 10 minutes later, and both calls are server-side only.
return_url must be https, or http on localhost. Without return_url, the sign-in goes back to the dashboard.
Your frontend: open the popup
Open the popup from a page on the return_url's origin: the popup takes the handoff token from that origin only. Wait for the popup's ready message, then post it the token:
const popup = window.open(
"about:blank",
"connect",
"popup,width=520,height=720",
);
const { launchUrl, handoffToken } = await startConnect(); // your backend makes both calls
const routerOrigin = new URL(launchUrl).origin;
popup.location = launchUrl;
window.addEventListener("message", (event) => {
if (event.source !== popup || event.origin !== routerOrigin) return;
if (event.data?.type !== "va.connector.oauth.ready") return;
popup.postMessage(
{ type: "va.connector.oauth.handoff", handoff_token: handoffToken },
routerOrigin,
);
});The handoff token works once.
Your return page: tell the opening page
When the sign-in ends, the popup lands on your return_url:
https://app.example.com/settings/integrations?connection_id=...&status=connectedstatus is connected or failed. A user who cancels at the provider comes back with failed.
Pass status on to the opening page, for instance with a same-origin BroadcastChannel, then close the popup.
Your backend: check the status
curl "$STREAM_ACCELERATION_URL/v1/agents/connections/$CONNECTION_ID" \
-H "api_key: $STREAM_API_KEY" -H "stream-auth-type: jwt" \
-H "Authorization: Bearer $SERVER_TOKEN" \
-H "X-Stream-User-Id: alice"status |
What it means | What to do |
|---|---|---|
pending |
Nobody has signed in yet. It stays pending after a failed sign-in |
Start a sign-in again |
connected |
The user signed in, and the agent can use the account | Nothing |
needs_reauthorization |
The provider no longer accepts the credential | Reconnect |
Reconnect
Start a new sign-in on the same connection, and open its launch_url the same way. Until the user finishes, the connection stays as it was.
OAuth clients
Stream's client needs no setup, so we recommend starting with it. Set your own client when you go to production. The catalog shows which connectors take one.
Your app's client is set on the connector under Library > Connectors (OAuth client), or from your backend. Its redirect URI is $STREAM_ACCELERATION_URL/v1/agents/connectors/oauth/callback. We encrypt the secret and never return it.
curl -X PUT "$STREAM_ACCELERATION_URL/v1/agents/connectors/google_calendar/oauth-client" \
-H "api_key: $STREAM_API_KEY" -H "stream-auth-type: jwt" \
-H "Authorization: Bearer $SERVER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"client_id": "...", "client_secret": "..."}'