---
canonical_url: "https://docs.composio.dev/docs/authentication/managed-auth-for-custom-tools"
markdown_url: "https://docs.composio.dev/docs/authentication/managed-auth-for-custom-tools.md"
product: "platform"
version: null
last_updated: null
---

# Managed authentication for your own tools (/docs/authentication/managed-auth-for-custom-tools)

Use Composio to connect your users' accounts and authenticate API requests from your own tools. Your tool definitions and business logic stay in your application. Composio stores and refreshes credentials, sends requests to the provider, and returns the response. You don't need predefined Composio tools, MCP, or an LLM.

## Connect an account and call its API [#connect-an-account-and-call-its-api]

This example connects GitHub and reads the connected account's profile through `GET /user`. Install the SDK with `pip install composio` or `npm install @composio/core`. Set `COMPOSIO_API_KEY` in your backend environment. The API example also needs Bash and `jq`.

For a [scoped project API key](/reference/authenticating-to-composio/project-api-key-permissions), enable **Session management → Write only**, **Connected accounts → Read only**, and **Proxy execute (Legacy) → Write only**. Default full-access project keys already include these permissions. Keep the key server-side.

The example uses `org_42:user_7` as an illustrative connection owner. In your application, derive this value from the authenticated request after checking workspace membership. Never accept a model-supplied or browser-supplied owner ID as authorization. See [B2B connection ownership](/docs/b2b-agents#choose-the-connection-owner) for member-owned and workspace-owned connections.

This example uses a session to create the Connect Link. Subsequent API calls pass the owner and connected-account ID directly.

**Python:**
```python
from composio import Composio

composio = Composio()

# Resolve this owner from your authenticated application session.
owner_id = "org_42:user_7"
session = composio.create(user_id=owner_id, toolkits=["github"])
connection = session.authorize("github")
print(f"Open this Connect Link: {connection.redirect_url}")
account = connection.wait_for_connection(60)

def read_github_profile(connected_account_id: str):
    return composio.tools.proxy(
        user_id=owner_id,
        connected_account_id=connected_account_id,
        endpoint="/user",
        method="GET",
    )

# Use the account returned by this owner's connection attempt.
response = read_github_profile(account.id)
print(response.status)
print(response.data)
```

**TypeScript:**
```typescript
import { Composio } from '@composio/core';

const composio = new Composio();

// Resolve this owner from your authenticated application session.
const ownerId = 'org_42:user_7';
const session = await composio.create(ownerId, { toolkits: ['github'] });
const connection = await session.authorize('github');
console.log(`Open this Connect Link: ${connection.redirectUrl}`);
const account = await connection.waitForConnection(60000);

function readGitHubProfile(connectedAccountId: string) {
  return composio.tools.proxyExecute({
    userId: ownerId,
    connectedAccountId,
    endpoint: '/user',
    method: 'GET',
  });
}

// Use the account returned by this owner's connection attempt.
const response = await readGitHubProfile(account.id);
console.log(response.status);
console.log(response.data);
```

**API:**
```bash
set -euo pipefail

# Resolve this owner from your authenticated application session.
OWNER_ID="org_42:user_7"
SESSION_ID=$(curl --fail-with-body -s -X POST https://backend.composio.dev/api/v3.1/tool_router/session \
  -H "x-api-key: $COMPOSIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"user_id\":\"$OWNER_ID\",\"toolkits\":{\"enable\":[\"github\"]}}" | jq -r '.session_id')

CONNECTION=$(curl --fail-with-body -s -X POST https://backend.composio.dev/api/v3.1/tool_router/session/$SESSION_ID/link \
  -H "x-api-key: $COMPOSIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"toolkit":"github"}')
echo "$CONNECTION" | jq -r '.redirect_url'
CONNECTED_ACCOUNT_ID=$(echo "$CONNECTION" | jq -r '.connected_account_id')

# Open the printed link. Wait at most 60 seconds for authorization.
DEADLINE=$((SECONDS + 60))
while true; do
  ACCOUNT=$(curl --fail-with-body -s https://backend.composio.dev/api/v3.1/connected_accounts/$CONNECTED_ACCOUNT_ID \
    -H "x-api-key: $COMPOSIO_API_KEY")
  STATUS=$(echo "$ACCOUNT" | jq -r '.status')
  case "$STATUS" in
    ACTIVE) break ;;
    FAILED|EXPIRED|REVOKED) echo "Connection ended: $STATUS" >&2; exit 1 ;;
  esac
  if [ "$SECONDS" -ge "$DEADLINE" ]; then
    echo "Timed out waiting for authorization" >&2
    exit 1
  fi
  sleep 1
done

# Use the account returned by this owner's connection attempt.
curl --fail-with-body -s -X POST https://backend.composio.dev/api/v3.1/tools/execute/proxy \
  -H "x-api-key: $COMPOSIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"user_id\":\"$OWNER_ID\",\"connected_account_id\":\"$CONNECTED_ACCOUNT_ID\",\"endpoint\":\"/user\",\"method\":\"GET\"}" \
  | jq '{status, data}'
```

Open the printed Connect Link and authorize GitHub within 60 seconds. The connection must become `ACTIVE` before the proxy request runs. During this wait, `INACTIVE` can still become `ACTIVE`, so keep polling until the deadline. Stop immediately on `FAILED`, `EXPIRED`, or `REVOKED`. The proxy returns the provider's response, not its OAuth token. No tool schemas are fetched and no model selects an action.

The [proxy response](/reference/api-reference/tools/postToolsExecuteProxy) contains the provider's response body in `data`, HTTP status in `status`, and response headers in `headers`.

The direct proxy call is independent of the session: pass both the owner (`user_id` in Python and REST, `userId` in TypeScript) and the exact returned connected-account ID. Projects with 2FA enabled check the owner against the connected account and may require it. Your own tool can call `read_github_profile()` or `readGitHubProfile()` from any framework, or you can make the equivalent REST request from your backend.

## Reuse connections in your application [#reuse-connections-in-your-application]

For onboarding or a settings page, send the Connect Link to the browser and persist the connection attempt server-side rather than holding an HTTP request open while the person signs in. When creating the Connect Link, pass your application's callback URL through `callback_url` in Python and REST, or `callbackUrl` in TypeScript. See [Redirecting users after authentication](/docs/authentication/manually-authenticating#redirecting-users-after-authentication) for setup.

On callback, verify that the connected-account ID belongs to that attempt and the signed-in owner, then retrieve its current status. Callback query parameters aren't proof of ownership. See [Identify the connected account](/docs/authentication/manually-authenticating#identify-the-connected-account).

Store the verified connected-account ID with your tenant/member mapping. On subsequent requests, authorize the caller and resolve that stored mapping before calling the proxy. Don't expose an endpoint that accepts arbitrary connected-account IDs from a browser or model.

If authorization fails or times out, don't call the provider. If a connection expires or is revoked, stop using it and ask its owner to reconnect. Subscribe to the [connection expiration event](/docs/authentication#connection-lifecycle) to surface reconnect prompts before a request fails. Don't automatically retry side-effecting provider requests; see [retry failed executions deliberately](/docs/production-readiness#retry-failed-executions-deliberately).

## Choose your OAuth app separately [#choose-your-oauth-app-separately]

Keep Composio-managed OAuth apps in production when their consent branding, scopes, and shared provider quotas fit your product. Use a [custom auth config](/docs/authentication/custom-app-vs-managed-app) when you need your own OAuth app, different scopes, dedicated quotas, or a custom provider instance. Composio still manages the resulting connected-account credentials in either case.

## Understand the boundaries [#understand-the-boundaries]

> **Managed authentication is not token export**: This is managed authentication for your own tools, not a token-export service. Connected-account reads redact credentials and aren't a supported way to retrieve access or refresh tokens. A custom OAuth app doesn't move token custody out of Composio Cloud. If your local vendor SDK requires a raw token, this proxy pattern doesn't supply one. Review [token custody and deployment options](/docs/security/token-custody) instead of attempting to extract credentials from account state.

* **Provider scope:** Use relative paths for the connected provider. Proxy Execute is not an arbitrary authenticated HTTP relay; absolute URLs must keep the connected provider's scheme and registrable domain.
* **Authorization:** Proxy permission can allow operations beyond predefined tool schemas. Validate endpoints and inputs in your own tools, enforce your application's tenant and role checks, and request only the provider scopes you need.
* **Deployment:** Private VPC and self-hosted deployments are Enterprise arrangements. Customer-managed keys require a separate deployment design with Composio. Confirm operator access, decryption, logs, and backups against your requirements.

## Next [#next]

- [Proxy Execute](/docs/extending-sessions/proxy-execute): Call provider endpoints from a session and understand proxy restrictions.

- [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits): Optionally expose your custom business logic through experimental session extension tools.

- [Token custody architecture](/docs/security/token-custody): Review credential redaction, OAuth app ownership, and deployment boundaries.

---

📚 **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

For agent tool delivery, Composio supports **Native Tools** (with a provider package) and **MCP** (no provider package needed).

For application-owned tools or deterministic backend code, use [managed authentication with Proxy Execute](/docs/authentication/managed-auth-for-custom-tools). A session can provide a user-scoped runtime context without an LLM or MCP. Composio authenticates provider requests server-side; connected-account reads redact credentials and aren't a supported token-export workflow.

### Native Tools

#### Python

```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

```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
```

#### API

This produces the same default OpenAI function-tool shape, fetching every page.

```bash
set -euo pipefail
SESSION=$(curl --fail-with-body -s -X POST https://backend.composio.dev/api/v3.1/tool_router/session \
  -H "x-api-key: $COMPOSIO_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"user_123"}')
SESSION_ID=$(echo "$SESSION" | jq -r '.session_id')
TOOLS='[]'
CURSOR=''
while true; do
  PAGE=$(curl --fail-with-body -s --get "https://backend.composio.dev/api/v3.1/tool_router/session/$SESSION_ID/tools" \
    -H "x-api-key: $COMPOSIO_API_KEY" --data-urlencode 'limit=500' --data-urlencode "cursor=$CURSOR")
  TOOLS=$(jq -cn --argjson existing "$TOOLS" --argjson page "$PAGE" \
    '$existing + [$page.items[] | {type:"function",function:{name:.slug,description:.description,parameters:.input_parameters}}]')
  CURSOR=$(echo "$PAGE" | jq -r '.next_cursor // empty')
  [ -n "$CURSOR" ] || break
done
printf '%s\n' "$TOOLS"
```

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.

