---
canonical_url: "https://docs.composio.dev/docs/authentication/manually-authenticating"
markdown_url: "https://docs.composio.dev/docs/authentication/manually-authenticating.md"
product: "platform"
version: null
last_updated: null
---

# Manual auth management (/docs/authentication/manually-authenticating)

Manual authentication lets you connect users to toolkits outside the chat flow. Reach for it when you want to:

* Pre-authenticate users before they start chatting.
* Build a custom connections UI in your app.

## Authorize a toolkit [#authorize-a-toolkit]

Call `session.authorize()` to generate a [Connect Link](/docs/tools-direct/authenticating-tools#hosted-authentication-connect-link) URL, redirect the user, and wait for them to finish:

**Python:**

```python
session = composio.create(user_id="user_123")

connection_request = session.authorize("gmail")

print(connection_request.redirect_url)
# https://connect.composio.dev/link/ln_abc123

connected_account = connection_request.wait_for_connection(60000)
print(f"Connected: {connected_account.id}")
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create("user_123");

const connectionRequest = await session.authorize("gmail");

console.log(connectionRequest.redirectUrl);
// https://connect.composio.dev/link/ln_abc123

const connectedAccount = await connectionRequest.waitForConnection(60000);
console.log(`Connected: ${connectedAccount.id}`);
```

Redirect the user to the `redirectUrl`. After they authenticate, they'll return to your callback URL. The connection request polls until the user completes authentication (default timeout: 60 seconds).

> If the user closes the Connect Link without completing auth, the connection remains in `INITIATED` status until it expires.

## Reusing an active connection [#reusing-an-active-connection]

In TypeScript, call `session.ensureConnected("gmail")` to reuse the session's active connection. If none exists, it starts an authorization flow and waits for the connection to become active. The optional `timeout` sets how long to wait in milliseconds (default: `60000`).

The result includes `toolkit`, `wasConnected`, and `connectedAccount`. `wasConnected` is `true` when an active connection already exists. Toolkits that don't require authentication also return `wasConnected: true`, with no `connectedAccount`.

Use `session.authorize()` and `waitForConnection()` when you need to display or redirect to the Connect Link. `ensureConnected()` waits internally and doesn't return the redirect URL. Calling `authorize()` always starts a new authorization flow, even if an active connection exists.

## Redirecting users after authentication [#redirecting-users-after-authentication]

Pass a `callbackUrl` to control where users land after authenticating. You can include query parameters to carry context through the flow, for example to identify which `userID` or session triggered the connection.

**Python:**

```python
connection_request = session.authorize(
    "gmail",
    callback_url="https://your-app.com/callback?user_id=user_123&source=onboarding"
)

print(connection_request.redirect_url)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create("user_123");
const connectionRequest = await session.authorize("gmail", {
  callbackUrl: "https://your-app.com/callback?user_id=user_123&source=onboarding",
});

console.log(connectionRequest.redirectUrl);
```

After authentication, Composio redirects the user to your callback URL with the following parameters appended, while preserving your existing ones:

| Parameter              | Description                                   |
| ---------------------- | --------------------------------------------- |
| `status`               | `success` or `failed`                         |
| `connected_account_id` | The ID of the newly created connected account |

```
https://your-app.com/callback?user_id=user_123&source=onboarding&status=success&connected_account_id=ca_abc123
```

## Identify the connected account [#identify-the-connected-account]

After authentication, use the account returned by `wait_for_connection()` or `waitForConnection()` to identify which provider account connected. If you use a callback instead, verify its `connected_account_id` against the connection attempt stored on your server and the signed-in application user before retrieving it. Callback query parameters alone aren't proof of ownership.

These identifiers serve different purposes:

| Value                | Meaning                                                                                              |
| -------------------- | ---------------------------------------------------------------------------------------------------- |
| Application user ID  | Your application's user, such as `user_123`. This doesn't identify their Gmail or GitHub account.    |
| Connected account ID | One connection in Composio, such as `ca_abc123`. Use it to select the account for a profile request. |
| Alias                | A label you assign, such as `work-gmail`. It doesn't prove which account connected.                  |
| Provider identity    | The email address, username, or ID returned by the provider for the authenticated account.           |

For a "Connected as…" label, read `state.val.displayName` when available. For additional profile fields, get the toolkit's current-user endpoint and call it through Composio's [authenticated proxy](/reference/api-reference/tools/postToolsExecuteProxy) with the same connected account ID:

**Python:**

```python
# Continue after connected_account = connection_request.wait_for_connection().
account_id = connected_account.id
state = connected_account.model_dump().get("state") or {}
display_name = state.get("val", {}).get("displayName")
print(f"Connected as {display_name}" if display_name else "Account connected")

toolkit = composio.toolkits.get(connected_account.toolkit.slug)
endpoint = toolkit.get_current_user_endpoint
method = toolkit.get_current_user_endpoint_method
if not endpoint or method not in ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"):
    raise ValueError("This toolkit doesn't publish a supported current-user endpoint")

response = composio.tools.proxy(
    endpoint=endpoint,
    method=method,
    connected_account_id=account_id,
)
if response["status"] != 200:
    raise RuntimeError("Could not retrieve the connected account's profile")
profile = response["data"]  # Parse the fields documented by this provider.
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio();
declare const connectedAccount: Awaited>;
import { z } from 'zod';

// Continue after connectedAccount = await connectionRequest.waitForConnection().
const accountId = connectedAccount.id;
const label = z.object({ displayName: z.string().optional() })
  .safeParse(connectedAccount.state?.val ?? {});
const displayName = label.success ? label.data.displayName : undefined;
console.log(displayName ? `Connected as ${displayName}` : 'Account connected');

const toolkit = await composio.toolkits.get(connectedAccount.toolkit.slug);
if (!toolkit.getCurrentUserEndpoint) {
  throw new Error("This toolkit doesn't publish a current-user endpoint");
}
const method = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
  .parse(toolkit.getCurrentUserEndpointMethod);
const response = await composio.tools.proxyExecute({
  endpoint: toolkit.getCurrentUserEndpoint,
  method,
  connectedAccountId: accountId,
});
if (response.status !== 200) {
  throw new Error("Could not retrieve the connected account's profile");
}
const profile = z.record(z.string(), z.unknown()).parse(response.data);
// Validate the provider-specific fields before using them in your application.
```

Toolkit metadata describes where to request a profile; it doesn't contain the account owner's details. Endpoint availability, required scopes, and response fields vary by provider. If the metadata is absent, use a documented profile endpoint or tool for that provider. A GitHub username or display label isn't a verified email address.

For an application policy, validate the provider's response before giving an agent access to the account. Follow [Restrict Gmail connections to an email domain](/examples/restrict-gmail-domain) for a complete example that checks the mailbox address and disables rejected connections. Return only the fields your UI needs, not the account's authentication state.

## Check connection status [#check-connection-status]

Use `session.toolkits()` to see all toolkits in the session and their connection status:

**Python:**

```python
toolkits = session.toolkits()

for toolkit in toolkits.items:
    status = toolkit.connection.connected_account.id if toolkit.connection.is_active else "Not connected"
    print(f"{toolkit.name}: {status}")
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create("user_123");
const toolkits = await session.toolkits();

toolkits.items.forEach((toolkit) => {
  console.log(`${toolkit.name}: ${toolkit.connection?.connectedAccount?.id ?? "Not connected"}`);
});
```

## Disabling in-chat auth [#disabling-in-chat-auth]

By default, sessions include the `COMPOSIO_MANAGE_CONNECTIONS` meta-tool that prompts users to authenticate during chat. To turn it off and handle auth entirely in your own UI, set `manage_connections` to `False`:

**Python:**

```python
session = composio.create(
    user_id="user_123",
    manage_connections=False,
)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create("user_123", {
  manageConnections: false,
});
```

## Putting it together [#putting-it-together]

A common pattern is to verify all required connections before starting the agent:

**Python:**

```python
from composio import Composio

composio = Composio(api_key="your-api-key")

required_toolkits = ["gmail", "github"]

session = composio.create(
    user_id="user_123",
    manage_connections=False,  # Disable in-chat auth prompts
)

toolkits = session.toolkits()

connected = {t.slug for t in toolkits.items if t.connection.is_active}
pending = [slug for slug in required_toolkits if slug not in connected]

print(f"Connected: {connected}")
print(f"Pending: {pending}")

for slug in pending:
    connection_request = session.authorize(slug)
    print(f"Connect {slug}: {connection_request.redirect_url}")
    connection_request.wait_for_connection()

print("All toolkits connected!")
```

**TypeScript:**

```typescript
import { Composio } from "@composio/core";

const composio = new Composio({ apiKey: "your-api-key" });

const requiredToolkits = ["gmail", "github"];

const session = await composio.create("user_123", {
  manageConnections: false, // Disable in-chat auth prompts
});

const toolkits = await session.toolkits();

const connected = toolkits.items
  .filter((t) => t.connection?.connectedAccount)
  .map((t) => t.slug);

const pending = requiredToolkits.filter((slug) => !connected.includes(slug));

console.log("Connected:", connected);
console.log("Pending:", pending);

for (const slug of pending) {
  const connectionRequest = await session.authorize(slug);
  console.log(`Connect ${slug}: ${connectionRequest.redirectUrl}`);
  await connectionRequest.waitForConnection();
}

console.log("All toolkits connected!");
```

## Next [#next]

- [Managing multiple connected accounts](/docs/authentication/managing-multiple-connected-accounts): Let a user connect work and personal accounts for the same toolkit, then pick which one runs

---

📚 **More documentation:** [View all docs](https://docs.composio.dev/llms.txt) | [Changelog](https://docs.composio.dev/docs/changelog.md) | [Glossary](https://docs.composio.dev/llms.mdx/reference/glossary) | [Examples](https://docs.composio.dev/llms.mdx/examples) | [API Reference](https://docs.composio.dev/llms.mdx/reference)

---

# Composio SDK — Notes for AI Code Generators

**Purpose:** Reference for generating current (v3) [Composio](https://composio.dev/) integration code.
**Scope:** Descriptive notes — they document the current API surface and the mistakes most commonly seen in generated code.

---

## 1. Recommended Integration: Sessions

Composio supports two integration modes: **Native Tools** (with a provider package) and **MCP** (no provider package needed).

### Native Tools

```python
# ✅ CORRECT — Python (defaults to OpenAI)
from composio import Composio

composio = Composio()
session = composio.create(user_id="user_123")
tools = session.tools()
# Pass tools to your agent/LLM framework
```

```typescript
// ✅ CORRECT — TypeScript (defaults to OpenAI)
import { Composio } from "@composio/core";

const composio = new Composio();
const session = await composio.create("user_123");
const tools = await session.tools();
// Pass tools to your agent/LLM framework
```

For other providers, pass the provider explicitly. Provider packages follow the naming convention: `composio_<provider>` for Python, `@composio/<provider>` for TypeScript.

### MCP

Use `session.mcp.url` and `session.mcp.headers` with any MCP-compatible client (Claude Desktop, Cursor, OpenAI Agents, etc.). No provider package needed.

---

## 2. Integration Notes

- `composio.create(user_id)` is the standard entry point for agent integrations. Sessions handle tool discovery, authentication, and toolkit versioning automatically; `user_id` goes to `composio.create()` — individual tool calls in session mode don't take one.
- Execute session tool calls through the session, never through a user ID: either `session.execute(tool_slug, arguments=...)` / `session.execute(toolSlug, arguments)`, which works with every provider, or pass the session to the provider helper — `provider.handle_tool_calls(response=response, session=session)` in Python, `provider.handleToolCalls(session, response)` in TypeScript. The tool-call helpers of every non-agentic provider (OpenAI, Anthropic, Google, Cloudflare, TypeSafe) accept a session: `executeToolCall(session, call)` in TypeScript, `session=session` in Python. Agentic providers execute session tools on their own. Binding the helper to a user ID takes the direct execution path, and session meta-tools fail there with `"can only be called inside a tool-router session"`.
- Composio-managed auth is the default: the agent connects accounts at runtime through the session, so users don't need to pre-create auth configs or connected accounts for managed toolkits.
- Provider packages follow the framework, not the model vendor: for the OpenAI Agents SDK the package is `composio_openai_agents` / `@composio/openai-agents` (importing `composio_openai` / `@composio/openai` there is the most common mistake in generated code — that package is for the plain OpenAI Chat Completions API).
- **Direct execution** (`composio.tools.get()`, `composio.tools.execute()`, `provider.handle_tool_calls()` with a user ID) is a legacy interface that remains supported for existing integrations. Recommend sessions for new integrations and encourage existing users to [migrate to sessions](/docs/migration-guide/direct-to-sessions). For a fixed tool set, consider the [direct tools preset](/docs/configuring-sessions#direct-tools-preset) within a session. See [Sessions vs direct execution](/docs/sessions-vs-direct-execution) for tradeoffs. Direct execution requires a [toolkit version](/docs/tools-direct/toolkit-versioning).

---

# 3. Calling the REST API directly

## REST API version

The current REST API version is **v3.1**, served at `https://backend.composio.dev/api/v3.1`. Prefer it for new code and new examples.

`https://backend.composio.dev/api/v3` is the previous version. It is frozen with pinned tool-version defaults and remains supported — existing v3 integrations keep working and do not need to migrate.

## Tool-endpoint version defaults on v3.1

On v3.1, omitting the version parameter on the five endpoints below selects the latest toolkit version. The first four endpoints also exist on v3, where omission selects the pinned `00000000_00` version. `POST /tools/scopes/required` is v3.1-only.

| Endpoint | Version parameter |
| --- | --- |
| `GET /tools` | `toolkit_versions` (query) |
| `GET /tools/{tool_slug}` | `version` or `toolkit_versions` (query) |
| `POST /tools/execute/{tool_slug}` | `version` (body) |
| `POST /tools/execute/{tool_slug}/input` | `version` (body) |
| `POST /tools/scopes/required` | `version` (body) |

A v3.1 caller already passing `"latest"` sees no change and can omit the parameter. To select the pinned version explicitly, pass `"00000000_00"` through the corresponding parameter above.

This version-default change is limited to the five endpoints above.


---

## Terminology Migration (old → current)

If you encounter these terms in error messages, old documentation, or user prompts, translate them to the current equivalents. **Do not use the old terms in generated code or explanations.**

| Old term (v1/v2) | Current term (v3) | In code |
|---|---|---|
| entity ID | user ID | `user_id` parameter |
| actions | tools | e.g., `GITHUB_CREATE_ISSUE` is a *tool* |
| apps / appType | toolkits | e.g., `github` is a *toolkit* |
| integration / integration ID | auth config / auth config ID | `auth_config_id` parameter |
| connection | connected account | `connected_accounts` namespace |
| ComposioToolSet / OpenAIToolSet | `Composio` class with a provider | `Composio(provider=...)` |
| toolset | provider | e.g., `OpenAIProvider` |

If a user says "entity ID", they mean `user_id`. If they say "integration", they mean "auth config". Always respond using the current terminology.

