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:

agents/assistant/agent.yaml
name: assistant # an internal assistant for your team
mode: text
llm: llm-fast
connectors:
  - linear # each user signs in to their own Linear

The 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 --dashboard

The 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 assistant
CONNECTOR  NAME    ACCOUNT          GRANT      REQUIRED  SANDBOX
linear     linear  Each user's own  All tools  no        no

No 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.

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:

connected.html
<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=connected

Your 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.

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=connected

status 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.

Going further