User connection
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.
A user connection is an account one of your users connects. The agent uses it only in that user's conversations.
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 --dashboardThe CLI prints the address of the agent's test chat in the Dashboard. Open it and ask: "Which Linear issues are assigned to me?". A Connect Linear button shows under the reply while the conversation runs. Sign in at Linear, and the button turns into "Linear connected" as the agent goes on to list your issues.
To check whose account the agent uses:
getstream agents tools assistantCONNECTOR NAME ACCOUNT GRANT REQUIRED SANDBOX
linear linear Each user's own All tools no noNo CLI command creates or lists a user connection: getstream agents connections acts only on the app's own.
Help one user in the Dashboard
In the Stream Dashboard, go to Agents > Library > Connectors and open Linear. Under Connections, filter by End user ID with the ID your server opens sessions for. The Status column shows whether that user's connection works.
"Needs reconnect" means Linear no longer accepts the user's sign-in. The user signs in again, as in Reconnect.
Connect in your app's chat with the API
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:
Not documented for Kotlin yet. Showing JavaScript.
Not documented for Swift yet. Showing JavaScript.
Not documented for Flutter yet. Showing JavaScript.
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 with the API
Your app can also show its own Connect button. Your server starts the sign-in, your app 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 server: 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 server: 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 app. 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.
In your app: 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:
Not documented for Kotlin yet. Showing JavaScript.
Not documented for Swift yet. Showing JavaScript.
Not documented for Flutter yet. Showing JavaScript.
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 server: 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 with the API
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
To put your app's name on the consent screen, set your own OAuth client. See Sign-in and OAuth clients.