# Composio Documentation > Composio powers 1,400+ toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action. --- # 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_` for Python, `@composio/` 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. Only the OpenAI and Anthropic helpers accept a session; with other providers, use `session.execute()`. 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 fully supported lower-level interface: your code picks the tool, no runtime discovery. It fits deterministic workflows and scripts; sessions fit agents that decide at runtime. The tradeoffs are documented at https://docs.composio.dev/docs/sessions-vs-direct-execution. Note that direct execution requires a toolkit version (https://docs.composio.dev/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. # Documentation --- # Welcome (/docs) ## Two ways to start ### Build with Composio **Platform** Build Composio into your own agent or application — tools, auth, and triggers for every one of your users. - [Quickstart](/docs/quickstart): Build an agent that discovers tools and works across your apps. - [Framework guides](/docs/providers): Use OpenAI, Anthropic, Vercel AI SDK, or another framework. - [Sessions via MCP](/docs/sessions-via-mcp): Expose a Composio session through a hosted MCP endpoint. ### Use Composio **For You** Use Composio from the agents you already have — Claude Code, Codex, Cursor, or your terminal. - [Agent plugins](/docs/agent-plugins): Install the native Composio plugin for Codex or Claude Code. - [Composio CLI](/docs/cli): Search, connect, and run tools from your terminal. - [Connect over MCP](/docs/composio-connect): Use Composio with Cursor or another existing MCP client. --- # Quickstart (/docs/quickstart) Build an agent that chooses Composio tools at runtime. Type a task, connect an app if needed, and continue in the same conversation. Pick your framework below. A Composio [session](/docs/how-composio-works) gives your agent tool discovery, account connections, and execution across [1000+ apps](/toolkits). It exposes only a small set of meta tools, so app schemas are loaded when the agent needs them. > The TypeScript SDK is ESM-only and requires Node.js 22.22.3 or newer. Use `import` syntax rather than CommonJS `require()`. ## OpenAI Agents #### Install **Python:** The Composio Python SDK requires **Python 3.10 or newer**. > **Do not install composio-core**: The current Python package is `composio`. The package `composio-core` is the legacy v1 SDK — it calls deprecated APIs and does not work with sessions. If an older tutorial or an AI coding assistant suggests it, install `composio` instead. See the [migration guide](/docs/migration-guide/new-sdk). **TypeScript:** #### Configure API Keys > Get your `COMPOSIO_API_KEY` from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-quickstart) and `OPENAI_API_KEY` from [OpenAI](https://platform.openai.com/api-keys). ```bash title=".env" COMPOSIO_API_KEY=your_composio_api_key OPENAI_API_KEY=your_openai_api_key ``` #### Create session and run agent **Python:** ```python from dotenv import load_dotenv from composio import Composio from agents import Agent, Runner, SQLiteSession from composio_openai_agents import OpenAIAgentsProvider load_dotenv() # Initialize Composio with OpenAI Agents provider composio = Composio(provider=OpenAIAgentsProvider()) # Create a session for your user user_id = "user_123" session = composio.sessions.create(user_id=user_id) tools = session.tools() # For multi-turn, store the session ID in your db and reuse instead of creating another session: # session_id = session.session_id # session = composio.use(session_id) agent = Agent( name="Personal Assistant", instructions=( "Use Composio tools to complete the request. " "If a connection is required, share its Connect Link and wait. " "Ask for confirmation before creating, updating, or deleting data." ), model="gpt-5.2", tools=tools, ) # Memory for multi-turn conversation memory = SQLiteSession("conversation") print(""" What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository' """) while True: user_input = input("You: ").strip() if user_input.lower() == "exit": break print("Assistant: ", end="", flush=True) result = Runner.run_sync(starting_agent=agent, input=user_input, session=memory) print(f"{result.final_output}\n") ``` **TypeScript:** ```typescript import "dotenv/config"; import { Composio } from "@composio/core"; import { Agent, run, MemorySession } from "@openai/agents"; import { OpenAIAgentsProvider } from "@composio/openai-agents"; import { createInterface } from "readline/promises"; // Initialize Composio with OpenAI Agents provider const composio = new Composio({ provider: new OpenAIAgentsProvider() }); // Create a session for your user const userId = "user_123"; const session = await composio.create(userId); const tools = await session.tools(); // For multi-turn, store the session ID in your db and reuse instead of calling create() again: // const sessionId = session.sessionId; // const session = await composio.use(sessionId); const agent = new Agent({ name: "Personal Assistant", instructions: "Use Composio tools to complete the request. " + "If a connection is required, share its Connect Link and wait. " + "Ask for confirmation before creating, updating, or deleting data.", model: "gpt-5.2", tools, }); const memory = new MemorySession(); const readline = createInterface({ input: process.stdin, output: process.stdout }); console.log(` What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository' `); while (true) { const input = (await readline.question("You: ")).trim(); if (input.toLowerCase() === "exit") break; process.stdout.write("Assistant: "); const result = await run(agent, input, { session: memory }); process.stdout.write(`${result.finalOutput}\n`); } readline.close(); ``` ## Claude Agent SDK #### Install **Python:** > **Do not install composio-core**: The current Python package is `composio`. The package `composio-core` is the legacy v1 SDK — it calls deprecated APIs and does not work with sessions. If an older tutorial or an AI coding assistant suggests it, install `composio` instead. See the [migration guide](/docs/migration-guide/new-sdk). **TypeScript:** #### Configure API Keys > Get your `COMPOSIO_API_KEY` from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-quickstart) and `ANTHROPIC_API_KEY` from [Anthropic](https://console.anthropic.com/settings/keys). ```bash title=".env" COMPOSIO_API_KEY=your_composio_api_key ANTHROPIC_API_KEY=your_anthropic_api_key ``` #### Create session and run agent **Python:** ```python import asyncio from dotenv import load_dotenv from composio import Composio from composio_claude_agent_sdk import ClaudeAgentSDKProvider from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, create_sdk_mcp_server, AssistantMessage, TextBlock load_dotenv() # Initialize Composio with Claude Agent SDK provider composio = Composio(provider=ClaudeAgentSDKProvider()) # Create a session for your user user_id = "user_123" session = composio.sessions.create(user_id=user_id) tools = session.tools() # For multi-turn, store the session ID in your db and reuse instead of creating another session: # session_id = session.session_id # session = composio.use(session_id) custom_server = create_sdk_mcp_server(name="composio", version="1.0.0", tools=tools) async def main(): options = ClaudeAgentOptions( system_prompt=( "Use Composio tools to complete the request. " "If a connection is required, share its Connect Link and wait. " "Ask for confirmation before creating, updating, or deleting data." ), permission_mode="bypassPermissions", mcp_servers={"composio": custom_server}, ) async with ClaudeSDKClient(options=options) as client: print(""" What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository' """) while True: user_input = input("You: ").strip() if user_input.lower() == "exit": break await client.query(user_input) print("Claude: ", end="", flush=True) async for message in client.receive_response(): if isinstance(message, AssistantMessage): for block in message.content: if isinstance(block, TextBlock): print(block.text, end="", flush=True) print() asyncio.run(main()) ``` **TypeScript:** ```typescript import "dotenv/config"; import { Composio } from "@composio/core"; import { ClaudeAgentSDKProvider } from "@composio/claude-agent-sdk"; import { createSdkMcpServer, query } from "@anthropic-ai/claude-agent-sdk"; import { createInterface } from "readline/promises"; // Initialize Composio with Claude Agent SDK provider const composio = new Composio({ provider: new ClaudeAgentSDKProvider() }); // Create a session for your user const userId = "user_123"; const session = await composio.create(userId); const tools = await session.tools(); // For multi-turn, store the session ID in your db and reuse instead of calling create() again: // const sessionId = session.sessionId; // const session = await composio.use(sessionId); const customServer = createSdkMcpServer({ name: "composio", version: "1.0.0", tools: tools, }); const readline = createInterface({ input: process.stdin, output: process.stdout }); console.log(` What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository and create a Google Sheet with the issues' `); let isFirstQuery = true; const options = { systemPrompt: "Use Composio tools to complete the request. " + "If a connection is required, share its Connect Link and wait. " + "Ask for confirmation before creating, updating, or deleting data.", mcpServers: { composio: customServer }, permissionMode: "bypassPermissions" as const, }; while (true) { const input = (await readline.question("You: ")).trim(); if (input.toLowerCase() === "exit") break; const queryOptions = isFirstQuery ? options : { ...options, continue: true }; isFirstQuery = false; process.stdout.write("Claude: "); for await (const stream of query({ prompt: input, options: queryOptions })) { if (stream.type === "assistant") { for (const block of stream.message.content) { if (block.type === "text") { process.stdout.write(block.text); } } } } console.log(); } readline.close(); ``` ## Vercel AI SDK #### Install #### Configure API Keys > Get your `COMPOSIO_API_KEY` from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-quickstart) and `ANTHROPIC_API_KEY` from [Anthropic](https://console.anthropic.com/settings/keys). ```bash title=".env" COMPOSIO_API_KEY=your_composio_api_key ANTHROPIC_API_KEY=your_anthropic_api_key ``` #### Create session and run agent ```typescript import "dotenv/config"; import { anthropic } from "@ai-sdk/anthropic"; import { Composio } from "@composio/core"; import { VercelProvider } from "@composio/vercel"; import { streamText, stepCountIs, type ModelMessage } from "ai"; import { createInterface } from "readline/promises"; // Initialize Composio with Vercel provider const composio = new Composio({ provider: new VercelProvider() }); // Create a session for your user const userId = "user_123"; const session = await composio.create(userId); const tools = await session.tools(); // For multi-turn, store the session ID in your db and reuse instead of calling create() again: // const sessionId = session.sessionId; // const session = await composio.use(sessionId); const readline = createInterface({ input: process.stdin, output: process.stdout }); console.log(` What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository' `); const messages: ModelMessage[] = []; while (true) { const input = (await readline.question("You: ")).trim(); if (input.toLowerCase() === "exit") break; messages.push({ role: "user", content: input }); process.stdout.write("Assistant: "); const result = await streamText({ system: "Use Composio tools to complete the request. " + "If a connection is required, share its Connect Link and wait. " + "Ask for confirmation before creating, updating, or deleting data.", model: anthropic("claude-sonnet-4-6"), messages, stopWhen: stepCountIs(10), onStepFinish: (step) => { for (const toolCall of step.toolCalls) { process.stdout.write(`\n[Using tool: ${toolCall.toolName}]`); } }, tools, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); messages.push(...(await result.response).messages); } readline.close(); ``` Ask for any task that needs an app tool: `Summarize my unread emails from today`, or `List all open issues on the composio github repository`. If a task needs access to an account, the agent gives you a Connect Link. Open it, approve the connection, and tell the agent to continue. ## What just happened? [#what-just-happened] * The provider you picked formats Composio tools for your framework and wires in execution. * `composio.sessions.create()` creates a Composio session for `user_123`. The session scopes connections and tool calls to that ID. * Composio sessions persist. In an application, store the session ID (`session.session_id` in Python, `session.sessionId` in TypeScript) and restore it with `composio.use(session_id)` instead of creating a new session for every turn. See [Reusing a session](/docs/how-composio-works#how-sessions-behave). * `session.tools()` gives the agent a small set of meta tools for finding, connecting, and running app tools. It does not load thousands of tool schemas into the model context. > **Use your application's user ID in production**: `user_123` is only for this local example. Replace it with a stable ID from your database. Each ID gets separate connections and tool calls. You can inspect the selected tools, inputs, responses, and timing with the [Logs API](/reference/api-reference/logs). ## Adapt the example [#adapt-the-example] - [Use another framework](/docs/providers): Choose OpenAI, Anthropic, Vercel AI SDK, LangChain, CrewAI, or another supported provider. - [Configure the session](/docs/configuring-sessions): Restrict toolkits, preload tools, or select connected accounts. - [Design authentication](/docs/authentication): Add Connect Links and per-user connections to your application. - [Expose an MCP endpoint](/docs/sessions-via-mcp): Use the same Composio session from an MCP-compatible application. --- # SDKs and frameworks (/docs/providers) Composio works with any AI framework. A provider is the adapter that turns Composio tools into the native tool format your framework expects, so you don't write glue code. Pick the provider that matches the SDK or agent framework you already use. Each one fetches tools, handles execution, and hands your agent objects it understands. If your framework isn't listed, you can [build your own provider](/docs/providers/custom-providers). > **Using TypeScript?**: The TypeScript SDK is ESM-only and requires Node.js 22.22.3 or newer. Use `import` instead of CommonJS `require()`. ## AI SDKs [#ai-sdks] Use these when you call a model SDK directly. ## Agent frameworks [#agent-frameworks] Use these when an agent framework orchestrates the tool calls for you. ## Custom [#custom] " languages="['Python', 'TypeScript']" /> --- # Anthropic (/docs/providers/anthropic) The Anthropic provider formats Composio tools for Claude and executes the tool calls Claude returns. It works two ways: * The [Claude Messages API](https://docs.anthropic.com/en/api/messages), where you run the tool-call loop yourself. * The [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview), where the SDK runs the loop and Composio tools are exposed as an in-process MCP server. Pick the tab that matches your integration. ### messages The `AnthropicProvider` transforms Composio tools into the format the Claude Messages API expects, then executes the tool calls Claude requests and shapes the results back into Messages API content blocks. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-anthropic) and `ANTHROPIC_API_KEY` with your [Anthropic API key](https://console.anthropic.com/settings/keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx ANTHROPIC_API_KEY=xxxxxxxxx ``` **Create session and run** > Passing a session to `handle_tool_calls` / `handleToolCalls` requires `composio` newer than 0.19.0 (Python) or `@composio/core` ≥ 0.17.0 with `@composio/anthropic` ≥ 0.11.0 (TypeScript). On earlier versions, execute session tools with [`session.execute()`](/docs/how-composio-works#executing-session-tools). **Python:** ```python import json import anthropic from composio import Composio from composio_anthropic import AnthropicProvider composio = Composio(provider=AnthropicProvider()) client = anthropic.Anthropic() # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() messages = [ {"role": "user", "content": "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'"} ] response = client.messages.create( model="claude-opus-4-6", max_tokens=4096, tools=tools, messages=messages, ) # Agentic loop: keep executing tool calls until the model responds with text while response.stop_reason == "tool_use": tool_use_blocks = [block for block in response.content if block.type == "tool_use"] results = composio.provider.handle_tool_calls(response=response, session=session) messages.append({"role": "assistant", "content": response.content}) messages.append({ "role": "user", "content": [ {"type": "tool_result", "tool_use_id": tool_use_blocks[i].id, "content": json.dumps(result)} for i, result in enumerate(results) ], }) response = client.messages.create( model="claude-opus-4-6", max_tokens=4096, tools=tools, messages=messages, ) # Print final response for block in response.content: if block.type == "text": print(block.text) ``` **TypeScript:** ```typescript import Anthropic from '@anthropic-ai/sdk'; import { Composio } from '@composio/core'; import { AnthropicProvider } from '@composio/anthropic'; const composio = new Composio({ provider: new AnthropicProvider(), }); const client = new Anthropic(); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const messages: Anthropic.MessageParam[] = [ { role: "user", content: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" }, ]; let response = await client.messages.create({ model: "claude-opus-4-6", max_tokens: 4096, tools: tools, messages: messages, }); // Agentic loop: keep executing tool calls until the model responds with text while (response.stop_reason === "tool_use") { const toolResults = await composio.provider.handleToolCalls(session, response); messages.push({ role: "assistant", content: response.content }); messages.push(...toolResults); response = await client.messages.create({ model: "claude-opus-4-6", max_tokens: 4096, tools: tools, messages: messages, }); } // Print final response for (const block of response.content) { if (block.type === "text") { console.log(block.text); } } ``` > Pass the session to `handleToolCalls` / `handle_tool_calls` when the model received tools from `session.tools()`. The helper preserves Anthropic input normalization and restored schema aliases before executing every call through that session. For tools fetched via [`tools.get`](/docs/tools-direct/executing-tools), pass the user ID instead. ### agent-sdk The `ClaudeAgentSDKProvider` exposes your Composio tools to the Claude Agent SDK as an in-process MCP server. You build the server with `create_sdk_mcp_server` (Python) or `createSdkMcpServer` (TypeScript), register it on the agent, and the SDK runs the tool-call loop for you. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-anthropic) and `ANTHROPIC_API_KEY` with your [Anthropic API key](https://console.anthropic.com/settings/keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx ANTHROPIC_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python import asyncio from composio import Composio from composio_claude_agent_sdk import ClaudeAgentSDKProvider from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, create_sdk_mcp_server composio = Composio(provider=ClaudeAgentSDKProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() tool_server = create_sdk_mcp_server(name="composio", version="1.0.0", tools=tools) async def main(): options = ClaudeAgentOptions( system_prompt="You are a helpful assistant", permission_mode="bypassPermissions", mcp_servers={"composio": tool_server}, ) async with ClaudeSDKClient(options=options) as client: await client.query("Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'") async for msg in client.receive_response(): print(msg) asyncio.run(main()) ``` **TypeScript:** ```js import { Composio } from '@composio/core'; import { ClaudeAgentSDKProvider } from '@composio/claude-agent-sdk'; import { createSdkMcpServer, query } from '@anthropic-ai/claude-agent-sdk'; const composio = new Composio({ provider: new ClaudeAgentSDKProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const toolServer = createSdkMcpServer({ name: "composio", version: "1.0.0", tools: tools, }); for await (const content of query({ prompt: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", options: { mcpServers: { composio: toolServer }, permissionMode: "bypassPermissions", }, })) { if (content.type === "assistant") { console.log("Claude:", content.message); } } ``` The provider registers each tool's complete object schema. Object arguments that declare no properties reach execution with arbitrary nested keys unchanged, while root-level constraints such as `additionalProperties` and `patternProperties` remain enforced. Invalid arguments return an error result, and the tool does not run. ## Provider specifics [#provider-specifics] A few things are specific to the Anthropic provider: * **Tool caching.** Pass `cacheTools: true` to the constructor (`new AnthropicProvider({ cacheTools: true })`) to attach Anthropic's ephemeral `cache_control` to every tool definition and tool-result block. This lets Claude reuse cached tool schemas across requests and can cut prompt cost when you send the same large tool set on every turn. * **`handleToolCalls` returns Messages API content, not raw strings.** TypeScript returns a ready-to-append `user` message of `tool_result` blocks. Python returns raw results in model-call order, which the sample wraps into `tool_result` blocks. * **String-encoded tool inputs are handled for you.** Claude occasionally emits a tool's `input` as a JSON string instead of an object. The provider normalizes it before both direct and session execution. * **`cacheTools` only.** The constructor takes no other options. There is no agentic execution in this provider; you run the loop (Messages API) or hand the loop to the Claude Agent SDK (Agent SDK tab). ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # OpenAI (/docs/providers/openai) The OpenAI provider formats Composio tools for OpenAI's function-calling and executes the tool calls the model returns. It works three ways: * The [Responses API](https://platform.openai.com/docs/api-reference/responses), the recommended way to build agentic flows, where you run the tool-call loop yourself. * The [Chat Completions API](https://platform.openai.com/docs/api-reference/chat), the classic message-based interface, where you also run the loop. * The [Agents SDK](https://openai.github.io/openai-agents-python/), where the SDK runs the loop and executes Composio tools for you. The OpenAI provider is the default provider for the Composio SDK, so you get it without configuring anything. Pick the tab that matches your integration. ### responses The `OpenAIResponsesProvider` transforms Composio tools into OpenAI's function-calling format for the Responses API, then executes the tool calls the model returns and shapes the results into `function_call_output` items you feed back in. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-openai) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** The [Responses API](https://platform.openai.com/docs/api-reference/responses) is the recommended way to build agentic flows with OpenAI. You pass `previous_response_id` on each turn so the model keeps the prior context, and you send back only the new `function_call_output` items. > Passing a session to `handle_tool_calls` / `handleToolCalls` requires `composio` newer than 0.19.0 (Python) or `@composio/core` ≥ 0.17.0 with `@composio/openai` ≥ 0.12.0 (TypeScript). On earlier versions, execute session tools with [`session.execute()`](/docs/how-composio-works#executing-session-tools). **Python:** ```python import json from openai import OpenAI from composio import Composio from composio_openai import OpenAIResponsesProvider composio = Composio(provider=OpenAIResponsesProvider()) client = OpenAI() # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() response = client.responses.create( model="gpt-5.2", tools=tools, input=[ { "role": "user", "content": "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" } ] ) # Agentic loop: keep executing tool calls until the model responds with text while True: tool_calls = [o for o in response.output if o.type == "function_call"] if not tool_calls: break results = composio.provider.handle_tool_calls(response=response, session=session) response = client.responses.create( model="gpt-5.2", tools=tools, previous_response_id=response.id, input=[ { "type": "function_call_output", "call_id": call.call_id, "output": json.dumps(results[i]), } for i, call in enumerate(tool_calls) ] ) # Print final response for item in response.output: if item.type == "message": print(item.content[0].text) ``` **TypeScript:** ```typescript import OpenAI from 'openai'; import { Composio } from '@composio/core'; import { OpenAIResponsesProvider } from '@composio/openai'; const composio = new Composio({ provider: new OpenAIResponsesProvider(), }); const client = new OpenAI(); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); let response = await client.responses.create({ model: "gpt-5.2", tools: tools, input: [ { role: "user", content: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" }, ], }); // Agentic loop: keep executing tool calls until the model responds with text while (true) { const toolCalls = response.output.filter((o) => o.type === "function_call"); if (toolCalls.length === 0) break; const outputs = await composio.provider.handleToolCalls(session, response.output); response = await client.responses.create({ model: "gpt-5.2", tools: tools, previous_response_id: response.id, input: outputs, }); } // Print final response for (const item of response.output) { if (item.type === "message") { const block = item.content[0]; if (block.type === "output_text") { console.log(block.text); } } } ``` ### chat The `OpenAIProvider` targets the Chat Completions API and is the default provider used by the Composio SDK when you do not specify one. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-openai) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** The [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) generates a model response from a list of messages. You keep the full message list yourself and append each assistant message and its `tool` results before the next call. > Passing a session to `handle_tool_calls` / `handleToolCalls` requires `composio` newer than 0.19.0 (Python) or `@composio/core` ≥ 0.17.0 with `@composio/openai` ≥ 0.12.0 (TypeScript). On earlier versions, execute session tools with [`session.execute()`](/docs/how-composio-works#executing-session-tools). **Python:** ```python import json from openai import OpenAI from composio import Composio from composio_openai import OpenAIProvider composio = Composio(provider=OpenAIProvider()) client = OpenAI() # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() messages = [ {"role": "user", "content": "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'"} ] response = client.chat.completions.create( model="gpt-5.2", tools=tools, messages=messages, ) # Agentic loop: keep executing tool calls until the model responds with text while response.choices[0].message.tool_calls: results = composio.provider.handle_tool_calls(response=response, session=session) messages.append(response.choices[0].message) for i, tc in enumerate(response.choices[0].message.tool_calls): messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(results[i]), }) response = client.chat.completions.create( model="gpt-5.2", tools=tools, messages=messages, ) print(response.choices[0].message.content) ``` **TypeScript:** ```typescript import OpenAI from 'openai'; import { Composio } from '@composio/core'; import { OpenAIProvider } from '@composio/openai'; const composio = new Composio({ provider: new OpenAIProvider(), }); const client = new OpenAI(); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ { role: "user", content: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" }, ]; let response = await client.chat.completions.create({ model: "gpt-5.2", tools: tools, messages: messages, }); // Agentic loop: keep executing tool calls until the model responds with text while (response.choices[0].message.tool_calls) { const results = await composio.provider.handleToolCalls(session, response); messages.push(response.choices[0].message); messages.push(...results); response = await client.chat.completions.create({ model: "gpt-5.2", tools: tools, messages: messages, }); } console.log(response.choices[0].message.content); ``` ### agents The `OpenAIAgentsProvider` transforms Composio tools into the Agents SDK tool format with execution built in, so the SDK runs the tool-call loop and you only define the agent and call `run`. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-openai) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python import asyncio from composio import Composio from composio_openai_agents import OpenAIAgentsProvider from agents import Agent, Runner composio = Composio(provider=OpenAIAgentsProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = Agent( name="Email Agent", instructions="You are a helpful assistant.", tools=tools, ) async def main(): result = await Runner.run( starting_agent=agent, input="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", ) print(result.final_output) asyncio.run(main()) ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; import { OpenAIAgentsProvider } from "@composio/openai-agents"; import { Agent, run } from "@openai/agents"; const composio = new Composio({ provider: new OpenAIAgentsProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const agent = new Agent({ name: "Email Agent", instructions: "You are a helpful assistant.", tools, }); const result = await run( agent, "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" ); console.log(result.finalOutput); ``` ## Provider specifics [#provider-specifics] The OpenAI integration ships three providers, one per API surface: * **`OpenAIResponsesProvider`** for the Responses API. `handleToolCalls` executes each `function_call` and returns `function_call_output` items keyed by `call_id`, paired with `previous_response_id` so you only resend new outputs each turn. * **`OpenAIProvider`** for the Chat Completions API. This is the SDK default, so `new Composio()` with no provider uses it. You keep the full message list and append each assistant message plus its `tool` results yourself. * **`OpenAIAgentsProvider`** for the Agents SDK. Tools come with execution wired in, so the SDK runs the loop for you. **Strict mode.** Pass `strict: true` or `strict=True` to `OpenAIResponsesProvider`, or pass `strict: true` to the TypeScript `OpenAIAgentsProvider`, to normalize each tool's input schema for [structured outputs](https://platform.openai.com/docs/guides/structured-outputs). The Python Agents SDK provider does not support strict mode yet. Every object lists all of its properties in `required` and is closed, while optional properties stay available but accept `null`. A `null` is dropped before the tool runs unless the tool's own schema accepts `null` for that parameter. Tools whose schema strict mode cannot express, such as objects that accept arbitrary keys, `allOf`, `prefixItems`, or unresolved `$ref`s, are sent without strict mode and log a warning. **Python:** ```python from composio import Composio from composio_openai import OpenAIResponsesProvider composio = Composio(provider=OpenAIResponsesProvider(strict=True)) ``` **TypeScript:** ```typescript // @noErrors import { Composio } from "@composio/core"; import { OpenAIResponsesProvider } from "@composio/openai"; const composio = new Composio({ provider: new OpenAIResponsesProvider({ strict: true }), }); ``` > Pass the session to `handleToolCalls` / `handle_tool_calls` when the model received tools from `session.tools()`. The helper preserves the provider's argument normalization and executes every call through that session. For tools fetched via [`tools.get`](/docs/tools-direct/executing-tools), pass the user ID instead. Use the Responses or Agents provider for new agentic flows; reach for Chat Completions when you are extending an existing Chat Completions codebase. ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Vercel AI SDK (/docs/providers/vercel) The Vercel AI SDK provider transforms Composio tools into Vercel's [tool format](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling) with built-in execution, so you don't write a manual agentic loop. Each wrapped tool carries its own `execute` function, and the AI SDK calls it for you. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-vercel) and `ANTHROPIC_API_KEY` with your [Anthropic API key](https://console.anthropic.com/settings/keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx ANTHROPIC_API_KEY=xxxxxxxxx ``` **Create session and run** The Vercel provider is **agentic**: tools include an `execute` function, so the AI SDK handles tool calls automatically. Set [`stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) to cap how many tool-calling steps a run can take. ```typescript import { anthropic } from "@ai-sdk/anthropic"; import { Composio } from "@composio/core"; import { VercelProvider } from "@composio/vercel"; import { generateText, stepCountIs } from "ai"; const composio = new Composio({ provider: new VercelProvider() }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const { text } = await generateText({ model: anthropic("claude-opus-4-6"), tools, prompt: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", stopWhen: stepCountIs(10), }); console.log(text); ``` ## Provider specifics [#provider-specifics] **Strict mode.** Some models reject tool schemas that contain optional parameters. Pass `strict: true` to the provider to normalize each tool's input schema for OpenAI structured outputs before it reaches the AI SDK: every object lists all of its properties in `required` and is closed, and optional properties stay available but accept `null`. A `null` is dropped before the tool runs unless the tool's own schema accepts `null` for that parameter, so nullable fields still receive an explicit `null`. Tools whose schema cannot be expressed in strict mode, such as objects that accept arbitrary keys, `allOf`, `prefixItems`, or unresolved `$ref`s, keep their original schema and log a warning: ```typescript // @noErrors import { Composio } from "@composio/core"; import { VercelProvider } from "@composio/vercel"; const composio = new Composio({ provider: new VercelProvider({ strict: true }) }); ``` > The provider converts each Composio tool's JSON Schema to a Zod schema for the AI SDK and normalizes tool arguments, so it still works when a model emits tool input as a JSON string rather than an object. ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Pi (/docs/providers/pi) The Pi provider adapts Composio tools for [`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent). A Pi session can search tools, manage connections, execute tools, and run the remote sandbox. Unlike the other providers, `PiProvider` ships its own hook interface to intercept, allow, deny, or rewrite every helper call before the model sees the result. > The Pi provider ships from `@composio/experimental` for TypeScript projects. ## Dynamic session helpers [#dynamic-session-helpers] For most Pi apps, expose the dynamic helper tools. They let the model discover exact Composio tool slugs before it executes, request missing connections, and run sandbox commands when you enable them. **Install** **Configure API keys** ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx ``` **Create a Composio session and Pi tools** ```typescript // @noErrors import { Composio } from '@composio/core'; import { PiProvider, createPiComposioSystemPrompt } from '@composio/experimental'; import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, } from '@earendil-works/pi-coding-agent'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY, provider: new PiProvider(), }); const composioSession = await composio.sessions.create('user_123', { toolkits: ['github', 'gmail'], manageConnections: { enable: true, callbackUrl: 'https://your-app.example.com/auth/callback', }, sandbox: { enable: true }, }); const composioTools = composio.provider.createSessionTools({ sessionId: composioSession.sessionId, search: composioSession.search.bind(composioSession), execute: composioSession.execute.bind(composioSession), callbackUrl: 'https://your-app.example.com/auth/callback', includeWorkbenchTools: true, connections: { getToolkitStates: toolkits => composioSession.toolkits({ toolkits }), authorizeToolkit: (toolkit, options) => composioSession.authorize(toolkit, options), }, hooks: { execute: (ctx, next) => { if (ctx.request.toolSlug === 'COMPOSIO_MANAGE_CONNECTIONS') { return ctx.deny('Use composio_manage_connections instead.'); } return next(); }, onAuthLink: async ctx => { await sendConnectionLinkToUser(ctx.url); return { message: 'Connection link sent out-of-band.' }; }, }, }); const loader = new DefaultResourceLoader({ cwd: process.cwd(), agentDir: getAgentDir(), systemPromptOverride: () => createPiComposioSystemPrompt(composioSession.sessionId, { includeWorkbenchTools: true, }), }); await loader.reload(); const { session: piSession } = await createAgentSession({ cwd: process.cwd(), resourceLoader: loader, sessionManager: SessionManager.inMemory(process.cwd()), customTools: composioTools, tools: [ 'composio_search_tools', 'composio_manage_connections', 'composio_execute_tool', 'composio_remote_workbench', 'composio_remote_bash', ], }); await piSession.prompt('Find my open GitHub issues and summarize the blockers.'); ``` The provider creates these Pi tools: * `composio_search_tools` searches Composio for exact tool slugs and schemas. * `composio_manage_connections` checks connection state and initiates auth for missing toolkits. * `composio_execute_tool` executes an exact Composio tool slug. * `composio_remote_workbench` runs Python in the Composio sandbox, and requires `includeWorkbenchTools: true`. * `composio_remote_bash` runs short bash commands in the sandbox filesystem, and requires `includeWorkbenchTools: true`. You can rename any of these helpers through the `names` option on `createSessionTools`, and the constants live on `PI_COMPOSIO_SESSION_TOOL_NAMES`. ## Hooks [#hooks] Hooks are the Pi provider's distinctive feature: middleware that wraps each helper so you control what runs and what the model sees. Pass a `hooks` object to `createSessionTools`. Each hook is `(ctx, next)`: `await next()` runs the default behavior, returning a value replaces what the model sees, and `ctx.deny(reason)` blocks the call. `ctx.request` is mutable; `ctx.context` is read-only. ```typescript // @noErrors const composioTools = composio.provider.createSessionTools({ sessionId: composioSession.sessionId, search: composioSession.search.bind(composioSession), execute: composioSession.execute.bind(composioSession), hooks: { search: (ctx, next) => { ctx.request.toolkits = ctx.request.toolkits?.map(toolkit => toolkit === 'slack' ? 'slackbot' : toolkit ); return next(); }, execute: async (ctx, next) => { if (ctx.request.toolSlug.startsWith('COMPOSIO_')) { return ctx.deny('Meta tools are blocked.'); } const result = await next(); const file = await saveLargeOutput(result); return file ? { message: `Output saved to ${file}` } : result; }, remoteBash: (ctx, next) => { if (ctx.request.command.includes('rm -rf')) { return ctx.deny('Destructive bash commands are blocked.'); } return next(); }, onAuthLink: async (ctx, next) => { await sendConnectionLinkToUser(ctx.url); return shouldShowLinkToModel(ctx) ? next() : { message: 'Connection link sent out-of-band.' }; }, }, }); ``` Available hooks, each keyed on `PiSessionHooks`: | Hook | Wraps | | ------------------- | -------------------------------------------------------- | | `search` | Tool discovery; rewrite `query` or `toolkits` | | `manageConnections` | Connection checks and auth | | `execute` | Tool execution; rewrite `toolSlug`, `args`, or `account` | | `remoteWorkbench` | The remote Python helper | | `remoteBash` | The remote bash helper | | `onAuthLink` | Any auth link found in a result | Every `ctx.request` is fully typed, so your editor surfaces the exact fields. `ctx.deny` is also exported as `denyPiToolCall(reason)`. ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Eve (/docs/providers/eve) The eve provider adapts Composio tools for the [eve](https://github.com/vercel/eve) agent framework. With `EveProvider` registered, `session.tools()` returns eve-native `defineTool`s, so an eve agent gets the Tool Router meta-tools and any preloaded custom toolkits from one call. The provider also ships a `(ctx, next)` hook interface to intercept, allow, deny, or rewrite meta-tool calls before the model sees the result. > The eve provider is experimental and currently ships from `@composio/experimental` for TypeScript projects. ## Usage [#usage] eve owns the agent loop and discovers tools from files, so you register the provider on the Composio client and expose the session's tools from a file under `agent/tools/`. **Install** ```bash npm install @composio/core @composio/experimental eve @ai-sdk/openai ``` This walkthrough uses OpenAI directly as one concrete model provider. OpenAI is only an example: you can use any model provider supported by eve by installing its AI SDK package and configuring the corresponding credential. **Configure credentials** ```txt title=".env.local" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` `COMPOSIO_API_KEY` authenticates Composio tools. `OPENAI_API_KEY` authenticates the model call directly with OpenAI; this setup does not route through Vercel AI Gateway. **Configure the eve model** ```typescript title="agent/agent.ts" import { openai } from '@ai-sdk/openai'; import { defineAgent } from 'eve'; export default defineAgent({ model: openai('gpt-5.4-mini'), }); ``` To use another provider, replace `@ai-sdk/openai`, `OPENAI_API_KEY`, and `openai(...)` with the equivalent package, credential, and model factory supported by eve. Passing a provider model object calls that provider directly; an eve string model ID such as `openai/gpt-5.4-mini` uses Vercel AI Gateway instead. **Register the provider and create a session** ```typescript title="agent/session.ts" // @noErrors import { Composio } from '@composio/core'; import { EveProvider } from '@composio/experimental/eve'; const composio = new Composio({ provider: new EveProvider() }); export const session = composio.sessions.create('user_123', { toolkits: ['github', 'hackernews'], // optional: scope the session to specific apps }); ``` **Expose the tools to eve** ```typescript title="agent/tools/composio.ts" // @noErrors import { defineComposioTools } from '@composio/experimental/eve'; import { session } from '../session'; export default defineComposioTools(session); ``` `defineComposioTools(session)` returns a `step.started` dynamic resolver and memoizes `session.tools()`. It resolves per step because the wrapped `execute` holds a live function eve keeps only for step-scoped tools. The fetch is cached per resolved Composio session, and transient failures are retried on the next step. For a multi-user channel, pass a resolver instead: `defineComposioTools((ctx) => sessionFor(ctx.session.auth.current?.principalId))`. Your agent now has the Tool Router meta-tools (`COMPOSIO_SEARCH_TOOLS`, `COMPOSIO_MULTI_EXECUTE_TOOL`, `COMPOSIO_MANAGE_CONNECTIONS`) plus any preloaded custom toolkits, with auth handled in chat. > eve's `defineTool` takes plain JSON Schema, so `EveProvider` passes Composio's `inputParameters` straight through. It does not convert to a zod schema, which would trip eve's dynamic-tool normalizer. ## Hooks [#hooks] Pass a `hooks` object to the constructor to wrap the Tool Router meta-tools. Each hook is `(ctx, next)`: `await next()` runs the default behavior, returning a value replaces what the model sees, and `ctx.deny(reason)` blocks the call. `ctx.request` is mutable; `ctx.context` is read-only. ```typescript // @noErrors import { EveProvider } from '@composio/experimental/eve'; const provider = new EveProvider({ hooks: { search: (ctx, next) => { ctx.request.args.toolkits = ['github']; return next(); }, remoteBash: async (ctx, next) => { if (String(ctx.request.args.command ?? '').includes('rm -rf')) { return ctx.deny('Destructive commands are blocked.'); } return next(); }, onAuthLink: async (ctx, next) => { await sendConnectionLinkToUser(ctx.url); return next(); }, }, }); ``` Available hooks, each keyed on `EveProviderHooks`: | Hook | Wraps | | ------------------- | --------------------------------------------------------- | | `search` | `COMPOSIO_SEARCH_TOOLS` | | `manageConnections` | `COMPOSIO_MANAGE_CONNECTIONS` | | `execute` | `COMPOSIO_MULTI_EXECUTE_TOOL` and `COMPOSIO_EXECUTE_TOOL` | | `remoteWorkbench` | `COMPOSIO_REMOTE_WORKBENCH` | | `remoteBash` | `COMPOSIO_REMOTE_BASH_TOOL` | | `onAuthLink` | Any auth link found in a result | `ctx.request` carries the meta-tool's raw `{ slug, args }`, and `ctx.deny` is also exported as `denyEveToolCall(reason)`. ### Require approval [#require-approval] Map Composio tools onto eve's durable approval flow with `needsApproval`. The callback receives the original Composio tool plus eve's approval context: ```typescript // @noErrors import { EveProvider, requireApprovalForTools } from '@composio/experimental/eve'; const provider = new EveProvider({ needsApproval: requireApprovalForTools('LOCAL_IMESSAGE_SEND'), }); ``` When this returns `true`, eve pauses before execution and asks the user to approve the call. `requireApprovalForTools` protects both direct calls and matching entries inside `COMPOSIO_MULTI_EXECUTE_TOOL`. Use an exact slug allowlist for side-effecting tools rather than approving every tool in a toolkit. ## Next [#next] - [iMessage on eve](/examples/imessage-agent): A full example: a custom toolkit for local iMessage plus the eve provider, on one session. - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Google (/docs/providers/google) The Google provider formats Composio tools for [Gemini](https://ai.google.dev/) and the [Google Agent Development Kit (ADK)](https://google.github.io/adk-docs/). Pick the tab that matches your setup. ### gemini In Python, the Gemini provider (`composio_gemini`) wraps Composio tools as typed callables, and the `google-genai` SDK's Automatic Function Calling executes tool calls inside the chat loop for you. In TypeScript, the Google provider transforms Composio tools into Gemini function declarations, and you run the loop: execute each call with `session.execute`, feed the result back, and repeat until the model replies with text. Object arguments that declare no properties preserve arbitrary nested keys in both providers. The TypeScript provider also adds `type: "object"` to schema nodes that declare `properties` without a type, which is the shape Gemini expects for object schemas. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-google) and `GOOGLE_API_KEY` with your [Google API key](https://aistudio.google.com/apikey). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx GOOGLE_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python from composio import Composio from composio_gemini import GeminiProvider from google import genai from google.genai import types composio = Composio(provider=GeminiProvider()) client = genai.Client() # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() config = types.GenerateContentConfig(tools=tools) chat = client.chats.create(model="gemini-3-pro-preview", config=config) # Automatic Function Calling executes tool calls inside the chat loop response = chat.send_message( "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" ) print(response.text) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; import { GoogleProvider } from '@composio/google'; import { GoogleGenAI, type Part } from '@google/genai'; const composio = new Composio({ provider: new GoogleProvider(), }); const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY! }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const chat = ai.chats.create({ model: 'gemini-3-pro-preview', config: { tools: [{ functionDeclarations: tools }], }, }); let response = await chat.sendMessage({ message: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", }); // Agentic loop: run each tool call with session.execute() until the model responds with text while (response.functionCalls && response.functionCalls.length > 0) { const parts: Part[] = []; for (const fc of response.functionCalls) { const result = await session.execute(fc.name || '', (fc.args || {}) as Record); parts.push({ functionResponse: { id: fc.id, name: fc.name, response: result.error ? { error: result.error } : result.data, }, }); } response = await chat.sendMessage({ message: parts }); } console.log(response.text); ``` > In TypeScript, execute session tools with `session.execute(toolSlug, arguments)`, as shown above. The provider's `executeToolCall` helper executes tools through the direct path, which does not carry your session — session meta-tools (`COMPOSIO_SEARCH_TOOLS`, `COMPOSIO_MANAGE_CONNECTIONS`, …) are rejected there with `"can only be called inside a tool-router session"`. Use `executeToolCall` only with tools fetched via [`tools.get`](/docs/tools-direct/executing-tools), not with `session.tools()`. Unlike the [OpenAI](/docs/providers/openai) and [Anthropic](/docs/providers/anthropic) helpers, the Google provider's `executeToolCall` does not yet accept a session. (The Python Gemini tab is unaffected: Automatic Function Calling runs provider-wrapped callables, which carry the session.) ### adk The Google ADK provider transforms Composio tools into ADK's `FunctionTool` format. Unlike Gemini function calling, ADK runs the tool loop for you: hand the tools to an `Agent`, and the `Runner` executes calls and continues until the agent produces a final response. ADK integration is Python-only. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-google) and `GOOGLE_API_KEY` with your [Google API key](https://aistudio.google.com/apikey). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx GOOGLE_API_KEY=xxxxxxxxx ``` **Create session and run** ```python from composio import Composio from composio_google_adk import GoogleAdkProvider from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types composio = Composio(provider=GoogleAdkProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = Agent( name="email_agent", model="gemini-3-pro-preview", instruction="You are an AI agent that sends emails using Gmail.", tools=tools, ) session_service = InMemorySessionService() adk_session = session_service.create_session_sync( app_name="email_agent", user_id="user_123", session_id="session_1", ) runner = Runner( agent=agent, app_name="email_agent", session_service=session_service, ) content = types.Content( role="user", parts=[types.Part(text="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'")], ) events = runner.run(user_id="user_123", session_id="session_1", new_message=content) for event in events: if event.is_final_response() and event.content and event.content.parts: print(event.content.parts[0].text) ``` ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # LangChain (/docs/providers/langchain) The LangChain provider formats Composio tools for [LangChain](https://python.langchain.com/) and [LangGraph](https://langchain-ai.github.io/langgraph/) agents. Pick the tab that matches your setup. ### langchain The LangChain provider transforms each Composio tool into a LangChain [`DynamicStructuredTool`](https://js.langchain.com/docs/concepts/tools/) with built-in execution. You can hand the tools to `create_agent` in Python or wire them into a graph node in TypeScript, and the framework runs the tool loop for you. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-langchain) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python from composio import Composio from composio_langchain import LangchainProvider from langchain.agents import create_agent from langchain_openai import ChatOpenAI composio = Composio(provider=LangchainProvider()) llm = ChatOpenAI(model="gpt-5.2") # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = create_agent(tools=tools, model=llm) result = agent.invoke({"messages": [("user", "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'")]}) print(result["messages"][-1].content) ``` **TypeScript:** ```typescript import { ChatOpenAI } from '@langchain/openai'; import { HumanMessage, AIMessage } from '@langchain/core/messages'; import { ToolNode } from '@langchain/langgraph/prebuilt'; import { StateGraph, MessagesAnnotation } from '@langchain/langgraph'; import { Composio } from '@composio/core'; import { LangchainProvider } from '@composio/langchain'; const composio = new Composio({ provider: new LangchainProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const toolNode = new ToolNode(tools); const model = new ChatOpenAI({ model: 'gpt-5.2', temperature: 0, }).bindTools(tools); function shouldContinue({ messages }: typeof MessagesAnnotation.State) { const lastMessage = messages[messages.length - 1] as AIMessage; if (lastMessage.tool_calls?.length) { return 'tools'; } return '__end__'; } async function callModel(state: typeof MessagesAnnotation.State) { const response = await model.invoke(state.messages); return { messages: [response] }; } const workflow = new StateGraph(MessagesAnnotation) .addNode('agent', callModel) .addEdge('__start__', 'agent') .addNode('tools', toolNode) .addEdge('tools', 'agent') .addConditionalEdges('agent', shouldContinue); const app = workflow.compile(); const finalState = await app.invoke({ messages: [new HumanMessage("Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'")], }); console.log(finalState.messages[finalState.messages.length - 1].content); ``` ### langgraph The LangGraph provider transforms Composio tools into the same LangChain `DynamicStructuredTool` format, ready to use with LangGraph agents. LangGraph integration is Python-only. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-langchain) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** ```python from composio import Composio from composio_langgraph import LanggraphProvider from langchain.agents import create_agent from langchain_openai import ChatOpenAI composio = Composio(provider=LanggraphProvider()) llm = ChatOpenAI(model="gpt-5.2") # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = create_agent(tools=tools, model=llm) result = agent.invoke({"messages": [("user", "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'")]}) print(result["messages"][-1].content) ``` ## Python argument validation [#python-argument-validation] The Python LangChain and LangGraph providers build a Pydantic argument model from each tool's JSON Schema. Object arguments that declare no properties accept and preserve arbitrary nested keys. For objects that declare named properties, undeclared keys are rejected unless the schema allows them with `additionalProperties`. Schemas that use `patternProperties` or schema-valued `additionalProperties` validate dynamic keys before execution. The providers also preserve whether an optional argument was omitted or explicitly set to `None`. By default, declared defaults are included, while optional arguments without defaults stay absent. Set `schema_config={"skip_defaults": True}` when creating the provider to leave declared defaults absent too. When validation fails, the tool does not run. LangChain surfaces the failure as a tool error observation so the agent can correct its arguments and retry. ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # AutoGen (/docs/providers/autogen) The AutoGen provider turns Composio tools into AutoGen [`FunctionTool`](https://microsoft.github.io/autogen/) objects and registers them with your agents. You connect an account, fetch the tools, register them with a caller and executor agent, and AutoGen handles the conversation and tool calls. The provider runs on the [`ag2`](https://github.com/ag2ai/ag2) distribution, the community-maintained continuation of AutoGen 0.2, so it works for AG2 projects out of the box. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-autogen) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** ```python import os from autogen import AssistantAgent, LLMConfig, UserProxyAgent from composio import Composio from composio_autogen import AutogenProvider composio = Composio(provider=AutogenProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() chatbot = AssistantAgent( "chatbot", system_message="Reply TERMINATE when the task is done or when user's content is empty", llm_config=LLMConfig({ "api_type": "openai", "model": "gpt-5.2", "api_key": os.environ["OPENAI_API_KEY"], }), ) user_proxy = UserProxyAgent( "user_proxy", is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content", "") or ""), human_input_mode="NEVER", code_execution_config={"use_docker": False}, ) # Register tools with both agents composio.provider.register_tools(caller=chatbot, executor=user_proxy, tools=tools) response = user_proxy.initiate_chat( chatbot, message="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", ) print(response.chat_history) ``` ## Provider specifics [#provider-specifics] AutoGen needs tools registered with two agents, not passed once. Call `composio.provider.register_tools(caller=..., executor=..., tools=tools)`: the `caller` decides which tool to invoke, and the `executor` runs it. Each tool comes back as an AutoGen `FunctionTool` with a generated `name`. AutoGen caps function names at 64 characters, so the provider hashes and truncates long tool slugs to stay under the limit. The registered name will not always match the original Composio slug. > `register_tools` is unique to the AutoGen provider. Other providers pass tools straight into the agent constructor, so don't expect this method elsewhere. ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # CrewAI (/docs/providers/crewai) The CrewAI provider turns Composio tools into CrewAI [`BaseTool`](https://docs.crewai.com/concepts/tools) objects that execute themselves. You connect an account, fetch the tools, pass them to an `Agent`, and CrewAI runs the task end to end. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-crewai) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** ```python from crewai import Agent, Crew, Task from composio import Composio from composio_crewai import CrewAIProvider composio = Composio(provider=CrewAIProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = Agent( role="Email Agent", goal="Send emails on behalf of the user", backstory="You are an AI agent that sends emails using Gmail.", tools=tools, llm="gpt-5.2", ) task = Task( description="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", agent=agent, expected_output="Confirmation that the email was sent", ) crew = Crew(agents=[agent], tasks=[task]) result = crew.kickoff() print(result) ``` ## Provider specifics [#provider-specifics] Each Composio tool becomes a CrewAI `BaseTool` whose `args_schema` is built from the tool's input schema, so CrewAI validates arguments before running anything. Object arguments that declare no properties accept and preserve arbitrary nested keys. For objects that declare named properties, undeclared keys are rejected unless the schema allows them with `additionalProperties`. Schemas that use `patternProperties` or schema-valued `additionalProperties` also validate dynamic keys before execution. The provider preserves whether an optional argument was omitted or explicitly set to `None`. By default, declared defaults are included, while optional arguments without defaults stay absent. Set `schema_config={"skip_defaults": True}` when creating the provider to leave declared defaults absent too. When validation fails, the tool does not raise. It returns a structured result instead: ```python {"successful": False, "error": "", "data": None} ``` Check `successful` in your task output rather than wrapping calls in `try`/`except`. ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # LlamaIndex (/docs/providers/llamaindex) The LlamaIndex provider turns Composio tools into LlamaIndex [`FunctionTool`](https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/) objects that execute themselves. You connect an account, fetch the tools, hand them to a `FunctionAgent`, and LlamaIndex drives the calls. The provider ships for both Python and TypeScript. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-llamaindex) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python import asyncio from composio import Composio from composio_llamaindex import LlamaIndexProvider from llama_index.core.agent.workflow import FunctionAgent from llama_index.llms.openai import OpenAI composio = Composio(provider=LlamaIndexProvider()) llm = OpenAI(model="gpt-5.2") # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = FunctionAgent(tools=tools, llm=llm) async def main(): result = await agent.run( user_msg="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" ) print(result) asyncio.run(main()) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; import { LlamaindexProvider } from '@composio/llamaindex'; import { openai } from '@llamaindex/openai'; import { agent } from '@llamaindex/workflow'; const composio = new Composio({ provider: new LlamaindexProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const myAgent = agent({ llm: openai({ model: 'gpt-5.2' }), tools, }); const result = await myAgent.run( "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" ); console.log(result.data.result); ``` ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Mastra (/docs/providers/mastra) The Mastra provider transforms Composio tools into [Mastra's tool format](https://mastra.ai/en/docs/tools-mcp/overview#creating-tools) with built-in execution. Pass the wrapped tools to a Mastra `Agent`, and the agent calls them automatically. Each tool gets both an input and an output schema, so Mastra can validate tool results as well as arguments. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=docs-providers-mastra) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** ```typescript import { Composio } from "@composio/core"; import { MastraProvider } from "@composio/mastra"; import { Agent } from "@mastra/core/agent"; import { openai } from "@ai-sdk/openai"; const composio = new Composio({ provider: new MastraProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const agent = new Agent({ id: "my-agent", name: "My Agent", instructions: "You are a helpful assistant.", model: openai("gpt-5.2"), tools, }); const { text } = await agent.generate([ { role: "user", content: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" }, ]); console.log(text); ``` ## Provider specifics [#provider-specifics] **Strict mode.** Pass `strict: true` to normalize each tool's input schema for OpenAI structured outputs before Mastra compiles it: every object lists all of its properties in `required` and is closed, and optional properties stay available but accept `null`. A `null` is dropped before the tool runs unless the tool's own schema accepts `null` for that parameter, so nullable fields still receive an explicit `null`. Tools whose schema cannot be expressed in strict mode, such as objects that accept arbitrary keys, `allOf`, `prefixItems`, or unresolved `$ref`s, keep their original schema and log a warning: ```typescript // @noErrors import { Composio } from "@composio/core"; import { MastraProvider } from "@composio/mastra"; const composio = new Composio({ provider: new MastraProvider({ strict: true }) }); ``` > The provider runs each tool's JSON Schema through Mastra's schema-compat layer and inlines internal `$ref` pointers first. A few Composio tools reference `$defs` entries that the upstream API does not emit. Rather than crash `tools.get`, the provider falls back to a permissive object schema for that property and logs one warning per tool, so the affected field validates loosely. ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Custom Providers (/docs/providers/custom-providers) Providers transform Composio tools into the format your AI framework expects. If your framework isn't listed in our supported providers, you can build your own. " /> " /> --- # TypeScript Custom Provider (/docs/providers/custom-providers/typescript) A **provider** adapts Composio tools to the format your AI framework expects. Write one, and any framework can call Composio's 1000+ tools. This guide shows you how to build your own in TypeScript. ## Provider architecture [#provider-architecture] A provider does three things: * **Transforms tool format**: converts Composio tools into the shape your AI platform expects. * **Executes tools**: runs tool calls and returns results. * **Adds platform helpers**: exposes convenience methods specific to your platform. There are two kinds, depending on whether the target platform runs its own agent loop: | Type | When to use | Examples | | --------------- | ------------------------------------------------------------ | ------------------ | | **Non-agentic** | The platform has no agency of its own. You drive the loop. | OpenAI | | **Agentic** | The platform runs its own agent loop and calls tools itself. | LangChain, AutoGPT | Both extend `BaseProvider`: ``` BaseProvider (Abstract) ├── BaseNonAgenticProvider (Abstract) │ └── OpenAIProvider (Concrete) │ └── [Your Custom Non-Agentic Provider] (Concrete) └── BaseAgenticProvider (Abstract) └── [Your Custom Agentic Provider] (Concrete) ``` ## Non-agentic provider [#non-agentic-provider] A non-agentic provider extends `BaseNonAgenticProvider`. You supply a `name`, `wrapTool`, and `wrapTools`, and call the built-in `executeTool` when you're ready to run a tool. ```typescript import { BaseNonAgenticProvider, Tool } from '@composio/core'; // Define your tool format interface MyAITool { name: string; description: string; parameters: { type: string; properties: Record; required?: string[]; }; } // Define your tool collection format type MyAIToolCollection = MyAITool[]; // Create your provider export class MyAIProvider extends BaseNonAgenticProvider { // Required: Unique provider name for telemetry readonly name = 'my-ai-platform'; // Required: Method to transform a single tool override wrapTool(tool: Tool): MyAITool { return { name: tool.slug, description: tool.description || '', parameters: { type: 'object', properties: tool.inputParameters?.properties || {}, required: tool.inputParameters?.required || [], }, }; } // Required: Method to transform a collection of tools override wrapTools(tools: Tool[]): MyAIToolCollection { return tools.map(tool => this.wrapTool(tool)); } // Optional: Custom helper methods for your AI platform async executeMyAIToolCall( userId: string, toolCall: { name: string; arguments: Record; } ): Promise { // Use the built-in executeTool method const result = await this.executeTool(toolCall.name, { userId, arguments: toolCall.arguments, }); return JSON.stringify(result.data); } } ``` ## Agentic provider [#agentic-provider] An **agentic provider** extends `BaseAgenticProvider`. The difference from the non-agentic case: `wrapTool` and `wrapTools` receive an `executeToolFn`, which you embed in each tool so the framework's agent can run the tool itself. ```typescript import { BaseAgenticProvider, Tool, ExecuteToolFn } from '@composio/core'; // Define your tool format interface AgentTool { name: string; description: string; execute: (args: Record) => Promise; schema: Record; } // Define your tool collection format interface AgentToolkit { tools: AgentTool[]; createAgent: (config: Record) => unknown; } // Create your provider export class MyAgentProvider extends BaseAgenticProvider { // Required: Unique provider name for telemetry readonly name = 'my-agent-platform'; // Required: Method to transform a single tool with execute function override wrapTool(tool: Tool, executeToolFn: ExecuteToolFn): AgentTool { return { name: tool.slug, description: tool.description || '', schema: tool.inputParameters || {}, execute: async (args: Record) => { const result = await executeToolFn(tool.slug, args); if (!result.successful) { throw new Error(result.error || 'Tool execution failed'); } return result.data; }, }; } // Required: Method to transform a collection of tools with execute function override wrapTools(tools: Tool[], executeToolFn: ExecuteToolFn): AgentToolkit { const agentTools = tools.map(tool => this.wrapTool(tool, executeToolFn)); return { tools: agentTools, createAgent: config => { // Create an agent using the tools return { run: async (prompt: string) => { // Implementation depends on your agent framework console.log(`Running agent with prompt: ${prompt}`); // The agent would use the tools.execute method to run tools }, }; }, }; } // Optional: Custom helper methods for your agent platform async runAgent(agentToolkit: AgentToolkit, prompt: string): Promise { const agent = agentToolkit.createAgent({}); return await agent.run(prompt); } } ``` ## Use your provider [#use-your-provider] Pass an instance to `Composio` via the `provider` option. Every tool you fetch comes back in your custom format. ```typescript import { Composio } from '@composio/core'; import { MyAIProvider } from './my-ai-provider'; // Create your provider instance const myProvider = new MyAIProvider(); // Initialize Composio with your provider const composio = new Composio({ apiKey: 'your-composio-api-key', provider: myProvider, }); // Get tools - they will be transformed by your provider const tools = await composio.tools.get('default', { toolkits: ['github'], }); // Use the tools with your AI platform console.log(tools); // These will be in your custom format ``` ## Provider state and context [#provider-state-and-context] A provider is a class, so it can hold state. Use the constructor for config, and instance fields for caches or counters. ```typescript export class StatefulProvider extends BaseNonAgenticProvider { readonly name = 'stateful-provider'; // Provider state private requestCount = 0; private toolCache = new Map(); private config: ProviderConfig; constructor(config: ProviderConfig) { super(); this.config = config; } override wrapTool(tool: Tool): ProviderTool { this.requestCount++; // Use the provider state/config const enhancedTool = { // Transform the tool name: this.config.useUpperCase ? tool.slug.toUpperCase() : tool.slug, description: tool.description, schema: tool.inputParameters, }; // Cache the transformed tool this.toolCache.set(tool.slug, enhancedTool); return enhancedTool; } override wrapTools(tools: Tool[]): ProviderToolCollection { return tools.map(tool => this.wrapTool(tool)); } // Custom methods that use provider state getRequestCount(): number { return this.requestCount; } getCachedTool(slug: string): ProviderTool | undefined { return this.toolCache.get(slug); } } ``` ## Add behavior with composition [#add-behavior-with-composition] Don't subclass a concrete provider to add analytics, retries, or other cross-cutting behavior. Hold a provider instance and delegate to it instead. This avoids making your class satisfy every overload that the provider exposes. ```typescript import { Composio } from '@composio/core'; import type { ExecuteToolFnOptions, ExecuteToolModifiers } from '@composio/core'; import { OpenAIProvider } from '@composio/openai'; import type OpenAI from 'openai'; class InstrumentedOpenAIToolExecutor { private readonly analytics = { toolCalls: 0, errors: 0, }; constructor(private readonly provider: OpenAIProvider) {} async executeToolCall( userId: string, tool: OpenAI.ChatCompletionMessageFunctionToolCall, options?: ExecuteToolFnOptions, modifiers?: ExecuteToolModifiers ): Promise { this.analytics.toolCalls++; try { return await this.provider.executeToolCall(userId, tool, options, modifiers); } catch (error) { this.analytics.errors++; throw error; } } getAnalytics(): Readonly<{ toolCalls: number; errors: number }> { return { ...this.analytics }; } } const provider = new OpenAIProvider(); const composio = new Composio({ provider }); const toolExecutor = new InstrumentedOpenAIToolExecutor(provider); ``` Pass the underlying provider to `Composio`, then call the wrapper from your manual tool-call loop. ## Best practices [#best-practices] * **Keep providers focused**: each provider should target one platform. * **Handle errors gracefully**: catch and transform errors from tool execution. * **Follow platform conventions**: adopt the naming and structure of the target platform. * **Cache transformed tools**: reuse wrapped tools instead of rebuilding them. * **Add helper methods**: expose convenience methods for common platform operations. * **Document your provider**: describe its features and usage. * **Set a meaningful `name`**: it's used for telemetry insights. --- # Python Custom Provider (/docs/providers/custom-providers/python) A **custom provider** adapts Composio tools to the format your AI framework expects, so you can use 1000+ tools with a platform Composio doesn't ship support for. This guide shows you how to build one in Python. ## Provider architecture [#provider-architecture] A provider does two things: 1. **Tool format transformation**: converts Composio tools into the format your AI platform understands. 2. **Platform-specific compatibility**: adds helper methods for executing tool calls and handling responses. There are two kinds of providers: | Type | Use it for | Examples | | --------------- | ------------------------------------------------------------------------- | ----------------- | | **Non-agentic** | Platforms that don't have their own agency. You drive the tool-call loop. | OpenAI, Anthropic | | **Agentic** | Platforms that run their own agent loop and call tools themselves. | LangChain, CrewAI | Each type extends a different abstract base class: ``` BaseProvider (Abstract) ├── NonAgenticProvider (Abstract) │ └── OpenAIProvider (Concrete) │ └── AnthropicProvider (Concrete) │ └── [Your Custom Non-Agentic Provider] (Concrete) └── AgenticProvider (Abstract) └── LangchainProvider (Concrete) └── [Your Custom Agentic Provider] (Concrete) ``` ## Quick start [#quick-start] Scaffold a working provider with the `make create-provider` script: ```bash # Create a non-agentic provider make create-provider name=myprovider # Create an agentic provider make create-provider name=myagent agentic=true # Write to a custom output directory make create-provider name=myprovider output=/path/to/custom/dir # Combine options make create-provider name=myagent agentic=true output=/my/custom/path ``` This generates a provider in `python/providers//` (or your `output` directory) with a `pyproject.toml`, a provider template, a demo script, a `README`, and type annotations. The template runs as-is. You implement the tool transformation for your platform, and you can keep your provider in your own repository. The generated structure: ``` python/providers// ├── README.md # Documentation and usage examples ├── pyproject.toml # Project configuration ├── setup.py # Setup script for pip compatibility ├── _demo.py # Demo script showing usage └── composio_/ # Package directory ├── __init__.py # Package initialization ├── provider.py # Provider implementation └── py.typed # PEP 561 type marker ``` From there: 1. Navigate to the provider directory: `cd python/providers/`. 2. Install in development mode: `uv pip install -e .`. 3. Implement your provider logic in `composio_/provider.py`. 4. Test with the demo script: `python _demo.py`. ## Creating a non-agentic provider [#creating-a-non-agentic-provider] A non-agentic provider extends `NonAgenticProvider` and implements two methods: `wrap_tool` (transform one tool) and `wrap_tools` (transform a collection). Add helper methods to execute tool calls in your platform's format. ```python from typing import List, Optional, Sequence, TypeAlias from composio.core.provider import NonAgenticProvider from composio.types import Tool, Modifiers, ToolExecutionResponse # Define your tool format class MyAITool: def __init__(self, name: str, description: str, parameters: dict): self.name = name self.description = description self.parameters = parameters # Define your tool collection format MyAIToolCollection: TypeAlias = List[MyAITool] # Create your provider class MyAIProvider(NonAgenticProvider[MyAITool, MyAIToolCollection], name="my-ai-platform"): """Custom provider for My AI Platform""" def wrap_tool(self, tool: Tool) -> MyAITool: """Transform a single tool to platform format""" return MyAITool( name=tool.slug, description=tool.description or "", parameters={ "type": "object", "properties": tool.input_parameters.get("properties", {}), "required": tool.input_parameters.get("required", []) } ) def wrap_tools(self, tools: Sequence[Tool]) -> MyAIToolCollection: """Transform a collection of tools""" return [self.wrap_tool(tool) for tool in tools] # Optional: Custom helper methods for your AI platform def execute_my_ai_tool_call( self, user_id: str, tool_call: dict, modifiers: Optional[Modifiers] = None ) -> ToolExecutionResponse: """Execute a tool call in your platform's format Example usage: result = my_provider.execute_my_ai_tool_call( user_id="default", tool_call={"name": "GITHUB_STAR_REPO", "arguments": {"owner": "composiohq", "repo": "composio"}} ) """ # Use the built-in execute_tool method return self.execute_tool( slug=tool_call["name"], arguments=tool_call["arguments"], modifiers=modifiers, user_id=user_id ) ``` ## Creating an agentic provider [#creating-an-agentic-provider] An agentic provider extends `AgenticProvider`. Because the platform calls tools itself, `wrap_tool` receives an `execute_tool` function that you wrap into a callable the framework can invoke directly. ```python from typing import Callable, Dict, List, Sequence from composio.core.provider import AgenticProvider, AgenticProviderExecuteFn from composio.types import Tool from my_provider import AgentTool # Import the Tool/Function class that represents a callable tool for your framework # Optionally define your custom tool format below # class AgentTool: # def __init__(self, name: str, description: str, execute: Callable, schema: dict): # self.name = name # self.description = description # self.execute = execute # self.schema = schema # Define your tool collection format (typically a List) AgentToolCollection: TypeAlias = List[AgentTool] # Create your provider class MyAgentProvider(AgenticProvider[AgentTool, List[AgentTool]], name="my-agent-platform"): """Custom provider for My Agent Platform""" def wrap_tool(self, tool: Tool, execute_tool: AgenticProviderExecuteFn) -> AgentTool: """Transform a single tool with execute function""" def execute_wrapper(**kwargs) -> Dict: result = execute_tool(tool.slug, kwargs) if not result.get("successful", False): raise Exception(result.get("error", "Tool execution failed")) return result.get("data", {}) return AgentTool( name=tool.slug, description=tool.description or "", execute=execute_wrapper, schema=tool.input_parameters ) def wrap_tools( self, tools: Sequence[Tool], execute_tool: AgenticProviderExecuteFn ) -> AgentToolCollection: """Transform a collection of tools with execute function""" return [self.wrap_tool(tool, execute_tool) for tool in tools] ``` ## Using your custom provider [#using-your-custom-provider] Pass an instance of your provider to `Composio`, and every tool you fetch comes back in your format. ### Non-agentic provider [#non-agentic-provider] You drive the loop: fetch tools, send them to the platform, then hand the response back to the provider to execute the calls. ```python from composio import Composio from composio_myai import MyAIProvider from myai import MyAIClient # Your AI platform's client # Initialize tools myai_client = MyAIClient() composio = Composio(provider=MyAIProvider()) # Define task task = "Star a repo composiohq/composio on GitHub" # Get GitHub tools that are pre-configured tools = composio.tools.get(user_id="default", toolkits=["GITHUB"]) # Get response from your AI platform (example) response = myai_client.chat.completions.create( model="your-model", tools=tools, # These are in your platform's format messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": task}, ], ) print(response) # Execute the function calls result = composio.provider.handle_tool_calls(response=response, user_id="default") print(result) ``` ### Agentic provider [#agentic-provider] The framework runs the loop. Fetch tools, attach them to an agent, and the agent calls them itself. ```python import asyncio from agents import Agent, Runner from composio_myagent import MyAgentProvider from composio import Composio # Initialize Composio toolset composio = Composio(provider=MyAgentProvider()) # Get all the tools tools = composio.tools.get( user_id="default", tools=["GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER"], ) # Create an agent with the tools agent = Agent( name="GitHub Agent", instructions="You are a helpful assistant that helps users with GitHub tasks.", tools=tools, ) # Run the agent async def main(): result = await Runner.run( starting_agent=agent, input=( "Star the repository composiohq/composio on GitHub. If done " "successfully, respond with 'Action executed successfully'" ), ) print(result.final_output) asyncio.run(main()) ``` ## Best practices [#best-practices] * **Keep providers focused**: each provider integrates with one platform. * **Handle errors gracefully**: catch and transform errors from tool execution. * **Follow platform conventions**: adopt the naming and structure of the target platform. * **Use type annotations**: lean on Python's typing for IDE support and documentation. * **Cache transformed tools**: store transformed tools when it helps performance. * **Add helper methods**: provide convenient methods for common platform operations. * **Document your provider**: include docstrings and usage examples. * **Set a meaningful provider name**: the `name` parameter is used for telemetry and debugging. --- # Agent plugins (/docs/agent-plugins) Agent plugins let Codex and Claude Code use Composio from your current conversation. The plugin teaches your agent how to find tools, connect accounts, and run actions through the Composio CLI. ## Install the plugin [#install-the-plugin] Install the Composio CLI: ```bash curl -fsSL https://composio.dev/install | sh ``` Open a new terminal, sign in, and configure every supported agent on your machine: ```bash composio login composio setup --target auto ``` `auto` detects Codex and Claude Code. If both are installed, it configures both. > **Running setup from an agent or script?**: Setup asks before changing local files. Add `--yes` in a non-interactive shell: `composio setup --target auto --yes`. ## Try a task [#try-a-task] Ask Codex or Claude Code to work with one of your apps: * `List the open GitHub issues assigned to me.` * `Summarize the unread Gmail messages I received today.` * `Create a Linear issue from these release notes: ...` You do not need to know a tool slug or connection ID. The agent searches by task and selects a matching tool. If the toolkit is not connected, the agent starts `composio link` and gives you a Connect Link. Approve the connection, then ask the agent to continue. ## Configure one agent [#configure-one-agent] Use an explicit target when you only want to configure one agent. ### Codex [#codex] ```bash composio setup --target codex ``` The Codex plugin bundles the Composio CLI skill. You can also install the plugin with Codex directly: ```bash codex plugin marketplace add https://github.com/ComposioHQ/composio-plugin-openai.git --json codex plugin add composio@composio --json ``` The plugin source is available in [`ComposioHQ/composio-plugin-openai`](https://github.com/ComposioHQ/composio-plugin-openai). ### Claude Code [#claude-code] ```bash composio setup --target claude ``` You can also install the plugin from Claude Code: ```bash /plugin marketplace add ComposioHQ/composio-plugin-cc /plugin install composio@composio ``` See the [Claude Code plugin guide](/docs/claude-code-plugin) for team installation, updates, and troubleshooting. ## What the plugin uses [#what-the-plugin-uses] Both plugins run the Composio CLI underneath. The agent uses `composio search` to find an unknown tool, `composio link` to connect an account, and `composio execute` to run a known tool. The same connections are available when you use the [CLI directly](/docs/cli). ## Install only the CLI skill [#install-only-the-cli-skill] The plugin includes a Composio CLI skill that teaches your agent how to search for and run tools. Codex receives it with the plugin, while `composio login` installs it for Claude Code by default. If you want the skill without the plugin, install it for your agent directly: ```bash composio --install-skill composio-cli claude composio --install-skill composio-cli codex ``` ## Choose another setup [#choose-another-setup] - [Build an agent](/docs/quickstart): Add Composio tools and authentication to your application. - [Use the CLI](/docs/cli): Search, connect, and run tools from your terminal. - [Connect over MCP](/docs/composio-connect): Add Composio to an existing MCP client. --- # Composio CLI (/docs/cli) The Composio CLI gives coding agents and terminals a local tool surface. Codex, Claude Code, or a person at the terminal can connect apps, execute tools, inspect schemas, call authenticated APIs, and debug Composio projects. It is also the runtime underneath the native [Composio agent plugins](/docs/agent-plugins). Reach for it when you want an agent to act in your connected apps directly, without building an SDK application or configuring MCP. ## Install [#install] Install the CLI with one command: ```bash curl -fsSL https://composio.dev/install | sh ``` The installer downloads and verifies the release bundle in `~/.composio`, creates the `~/.local/bin/composio` entry point, and configures your shell so future terminals find `composio` on `PATH`. It recognizes `zsh`, `bash`, and `fish` login shells from `$SHELL` and writes a managed `# Composio CLI` block to their startup files. `bash` also gets a login-mode startup file — the first existing of `~/.bash_profile` or `~/.bash_login`, or a newly created `~/.bash_profile` — because a login bash, which macOS Terminal.app starts, never reads `~/.bashrc`. If your shell is not recognized, or shell setup fails, the CLI still installs and the installer prints a runnable command instead. It does not install agent plugins or log you in unless you ask it to. To skip shell configuration entirely, see [Shell setup overrides](#shell-setup-overrides). Open a new terminal, then log in: ```bash composio login ``` Install the native plugin for Codex or Claude Code: ```bash composio setup --target auto ``` `auto` detects supported agents on your machine. Use `--target codex` or `--target claude` to configure only one. See [Agent plugins](/docs/agent-plugins) for manual plugin commands and host-specific details. Setup asks before changing local files. Add `--yes` when you run it from an agent, script, CI job, or another non-interactive shell. `composio login` installs the standalone `composio-cli` skill for Claude Code by default. For Codex, prefer `composio setup --target codex`; it installs the native plugin, which bundles the CLI skill. If you explicitly need the standalone skill without the native plugin, install it manually for the agent host: ```bash composio --install-skill composio-cli claude composio --install-skill composio-cli codex ``` ### Install with options [#install-with-options] Pin a version and opt in to agent plugin setup: ```bash curl -fsSL https://composio.dev/install \ | COMPOSIO_INSTALL_VERSION=0.3.1 COMPOSIO_INSTALL_PLUGINS=1 sh ``` You can also pass a stable or beta release tag as the positional argument. The positional value takes precedence over `COMPOSIO_INSTALL_VERSION`: ```bash curl -fsSL https://composio.dev/install | sh -s -- @composio/cli@0.3.1 ``` | Variable or argument | Description | Default | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | `COMPOSIO_INSTALL_DIR` | Directory that receives the complete CLI bundle and `release-tag.txt`. | `$HOME/.composio` | | `COMPOSIO_BIN_DIR` | Directory that receives the `composio` entry-point symlink. Set it to `COMPOSIO_INSTALL_DIR` to use the legacy single-directory layout. Treat this as trusted input: anyone who can write to this directory can replace commands that future terminals run. | `$HOME/.local/bin` | | `COMPOSIO_INSTALL_VERSION` | Stable or beta version to install, with or without the `@composio/cli@` prefix. | Latest stable release | | `version-tag` | Positional stable or beta version. This overrides `COMPOSIO_INSTALL_VERSION`. | None | | `COMPOSIO_QUIET` | Set to `1` or `true` to hide progress output. Warnings and errors still print. | Unset | | `COMPOSIO_DEBUG` | Set to `1` or `true` to print download URLs and temporary paths. | Unset | | `COMPOSIO_INSTALL_HELP` | Set to `0` to hide normal post-install guidance. Shell-setup failures still warn and print a recovery command to stderr. | `1` | | `COMPOSIO_INSTALL_PLUGINS` | Set to `1` to run `composio setup --target auto --yes --if-present` after installation. | `0` | | `COMPOSIO_INSTALL_SHELL` | Shell setup mode: `auto` infers your login shell from `$SHELL`, `zsh`, `bash`, or `fish` force a specific shell, and `none` skips shell configuration. See [Shell setup overrides](#shell-setup-overrides). | `auto` | | `COMPOSIO_GITHUB_OWNER` | GitHub owner used to resolve releases. | `ComposioHQ` | | `COMPOSIO_GITHUB_REPO` | GitHub repository used to resolve releases. | `composio` | | `COMPOSIO_GITHUB_URL` | GitHub web and release-download base URL. | `https://github.com` | | `COMPOSIO_GITHUB_API_BASE_URL` | GitHub API base URL. | Derived from `COMPOSIO_GITHUB_URL` | | `--agent` | Log in as a Composio agent after installation. | Off | | `--no-plugins` | Skip agent plugin setup. Kept for compatibility and now matches the default. | Off | ### Shell setup overrides [#shell-setup-overrides] By default the installer infers your login shell from `$SHELL` and configures it. Set `COMPOSIO_INSTALL_SHELL` to force a specific shell instead: **zsh:** ```bash curl -fsSL https://composio.dev/install | COMPOSIO_INSTALL_SHELL=zsh sh ``` This configures `~/.zshrc` and delegates setup to `composio install --shell zsh`. **bash:** ```bash curl -fsSL https://composio.dev/install | COMPOSIO_INSTALL_SHELL=bash sh ``` This configures `~/.bashrc` and delegates setup to `composio install --shell bash`. It also configures a login-mode startup file so `bash -ilc` can find `composio`: the first active login file (`~/.bash_profile`, then `~/.bash_login`), or a new `~/.bash_profile` that sources your existing `~/.profile` when neither exists. **fish:** ```bash curl -fsSL https://composio.dev/install | COMPOSIO_INSTALL_SHELL=fish sh ``` This configures `~/.config/fish/config.fish` and delegates setup to `composio install --shell fish`. Set `COMPOSIO_INSTALL_SHELL=none` for an install-only run that changes no shell files. Use it in CI, Docker images, or when a dotfile manager owns your startup files: ```bash curl -fsSL https://composio.dev/install | COMPOSIO_INSTALL_SHELL=none sh ``` Shell-specific installer variants (`zsh.sh`, `bash.sh`, and `fish.sh` in the repository's [`install/` directory](https://github.com/ComposioHQ/composio/tree/next/install)) pin `COMPOSIO_INSTALL_SHELL` to their shell before delegating to the base installer. Shell setup is idempotent: repeated installs keep exactly one managed PATH block per startup file and reconcile it when the bin directory changes. Setup falls back to writing the same PATH block inline when the installed CLI predates `composio install --shell`, delegated setup fails, or delegated setup leaves a stale block. Startup-file changes only affect future terminals; in the current one, either open a new terminal or run the absolute path the installer prints. ### Verify the installation [#verify-the-installation] ```bash composio --version which composio ``` The installer supports Linux x64, Linux ARM64, macOS Intel, and macOS Apple Silicon. On Windows, install and run it inside [WSL](https://learn.microsoft.com/windows/wsl/install). ### Update [#update] ```bash composio upgrade ``` This replaces the bundle in `~/.composio` in place and leaves the `~/.local/bin/composio` entry point pointing at it. Pass a version to pin a specific release (`composio upgrade 0.3.1`), or `--beta` for the latest prerelease. ### Install manually from GitHub Releases [#install-manually-from-github-releases] Download the archive for your platform from [GitHub Releases](https://github.com/ComposioHQ/composio/releases), then install the complete bundle. Keep the support files beside the executable. ```bash bundle=composio-linux-x64 COMPOSIO_INSTALL_DIR=${COMPOSIO_INSTALL_DIR:-"$HOME/.composio"} COMPOSIO_BIN_DIR=${COMPOSIO_BIN_DIR:-"$HOME/.local/bin"} unzip "$bundle.zip" mkdir -p "$COMPOSIO_INSTALL_DIR" cp -Rp "$bundle"/. "$COMPOSIO_INSTALL_DIR/" chmod +x "$COMPOSIO_INSTALL_DIR/composio" mkdir -p "$COMPOSIO_BIN_DIR" if [ "$COMPOSIO_BIN_DIR" != "$COMPOSIO_INSTALL_DIR" ]; then ln -sf "$COMPOSIO_INSTALL_DIR/composio" "$COMPOSIO_BIN_DIR/composio" fi export PATH="$COMPOSIO_BIN_DIR:$PATH" ``` ### Uninstall [#uninstall] Remove only installer-owned entry points and release artifacts. This keeps your credentials, configuration, and cache data in `~/.composio`. The file list below matches the current release layout; if you installed a different version, compare it against the contents of that release's archive. ```bash install_dir=${COMPOSIO_INSTALL_DIR:-"$HOME/.composio"} bin_dir=${COMPOSIO_BIN_DIR:-"$HOME/.local/bin"} rm -f \ "$bin_dir/composio" \ "$install_dir/composio" \ "$install_dir/release-tag.txt" \ "$install_dir/run-helpers-runtime.mjs" \ "$install_dir/run-subagent-shared.mjs" \ "$install_dir/run-subagent-acp.mjs" \ "$install_dir/run-subagent-legacy.mjs" \ "$install_dir/run-subagent-output-mcp.mjs" rm -rf \ "$install_dir/services" \ "$install_dir/acp-adapters" \ "$install_dir/local-tools-binaries" for file in \ "$HOME/.zshrc" \ "$HOME/.bashrc" \ "$HOME/.bash_profile" \ "$HOME/.bash_login" \ "$HOME/.config/fish/config.fish"; do [ -f "$file" ] || continue tmp=$(mktemp) || continue if awk ' $0 == "# Composio CLI" { in_block = 1; next } in_block && (/^export COMPOSIO_INSTALL_DIR=/ || /^set --export COMPOSIO_INSTALL_DIR /) { next } in_block && (/^export PATH=/ || /^set --export PATH /) { in_block = 0; next } { in_block = 0; print } ' "$file" > "$tmp"; then if [ "$file" = "$HOME/.bash_profile" ] && ! grep -q '[^[:space:]]' "$tmp"; then rm -f "$file" else cat "$tmp" > "$file" fi fi rm -f "$tmp" done ``` The loop stages every rewrite in a `mktemp` scratch file — created with an unpredictable name and `0600` permissions, so startup-file contents never pass through a world-readable path — and writes the result back only when `awk` succeeds. A failed or missing `awk` leaves the startup file untouched, and the scratch file is always removed. Writing back with `cat` keeps a symlinked startup file intact: the symlink, its target's inode, owner, and mode all survive. The filter removes the current managed block and the three-line block written by older installers (marker plus `export COMPOSIO_INSTALL_DIR=...` or `set --export COMPOSIO_INSTALL_DIR ...`). `~/.bash_profile` gets one extra step: when removing the block leaves only blank lines, the file is deleted. The installer creates `~/.bash_profile` on bash systems that had no login startup file, and bash prefers even an empty `~/.bash_profile` over `~/.profile`, so leaving the empty file behind would silently override bash's normal startup-file selection forever. Note the edge case: a `~/.bash_profile` you created yourself but left empty is also removed. If you had a `~/.profile` when you installed, the created `~/.bash_profile` instead keeps a passthrough that sources it, so the file is not blank and survives the loop with only the block removed. It begins with `# Created by the Composio CLI installer.`; delete it too if you want bash to read `~/.profile` directly again. > **Purge all CLI state** The command below also deletes saved credentials, configuration, and caches. Run it only when you want a complete reset. ```bash rm -rf "${COMPOSIO_INSTALL_DIR:-$HOME/.composio}" ``` ## Agent and terminal workflows [#agent-and-terminal-workflows] The CLI executes tools, connects accounts, scripts workflows, calls authenticated APIs, and inspects trigger events without you wiring up a custom integration first. Native Codex and Claude Code plugins expose these capabilities to the agent; the same commands work directly in your terminal. ### Search, connect, and execute tools [#search-connect-and-execute-tools] Use this flow when you or your agent needs to act in one of your connected apps: ```bash # Find the right tool composio search "summarize my unread gmail" # Inspect the required input schema composio execute GMAIL_FETCH_EMAILS --get-schema # Connect the app if needed composio link gmail # Execute the tool composio execute GMAIL_FETCH_EMAILS \ -d '{ query: "is:unread newer_than:1d", max_results: 10 }' ``` The commands you'll reach for most: | Command | Use it for | | ------------------ | --------------------------------------------- | | `composio search` | Find relevant tools by natural language | | `composio execute` | Execute a known tool slug | | `composio link` | Connect an app account | | `composio proxy` | Call provider APIs with Composio-managed auth | Use `composio proxy` when the agent already knows the provider's API endpoint and just needs Composio to inject auth from your connected account: ```bash composio proxy https://gmail.googleapis.com/gmail/v1/users/me/profile --toolkit gmail ``` ### Run scripts and sub-agents [#run-scripts-and-sub-agents] Reach for `composio run` when the agent needs a multi-step workflow: loops, parallel fan-out, data transformation, or LLM-assisted summarization. It runs inline TS/JS or a file, with `execute()`, `search()`, `proxy()`, `experimental_subAgent()`, `result.prompt()`, and `z` injected. Run a single scripted workflow: ```bash composio run ' const messages = await execute("GMAIL_FETCH_EMAILS", { query: "is:unread newer_than:1d", max_results: 10, }); console.log(messages); ' ``` Fan out across multiple tools: ```bash composio run ' const [emails, issues, events] = await Promise.all([ execute("GMAIL_FETCH_EMAILS", { max_results: 5 }), execute("GITHUB_LIST_REPOSITORY_ISSUES", { owner: "composiohq", repo: "composio", state: "open" }), execute("GOOGLECALENDAR_FIND_EVENT", { calendar_id: "primary" }), ]); console.log({ emails: emails.data, issues: issues.data, events: events.data }); ' ``` Ask a sub-agent to summarize tool output and return structured data: ```bash composio run --logs-off ' const [emails, issues] = await Promise.all([ execute("GMAIL_FETCH_EMAILS", { max_results: 5 }), execute("GITHUB_LIST_REPOSITORY_ISSUES", { owner: "composiohq", repo: "composio", state: "open" }), ]); const brief = await experimental_subAgent( `Create a morning brief from these emails and issues.\n\n${emails.prompt()}\n\n${issues.prompt()}`, { schema: z.object({ brief: z.string(), urgentEmails: z.array(z.string()), urgentIssues: z.array(z.string()), }), } ); console.log(brief.structuredOutput); ' ``` Run a checked-in script: ```bash composio run --file ./workflow.ts -- --repo composiohq/composio ``` ### Listen to trigger events [#listen-to-trigger-events] Use trigger listening when the agent needs to wait for new events, inspect incoming payloads, or forward events while debugging. Event streaming lives in the developer namespace: ```bash # Compact table view for matching events composio dev listen --toolkits gmail --table # Raw JSON payloads, then stop after five events composio dev listen --trigger-slug GMAIL_NEW_GMAIL_MESSAGE --json --max-events 5 # Forward each matching event to a local or hosted webhook composio dev listen --toolkits github --forward https://example.com/webhook # Append matching events to a local file for an agent to inspect composio dev listen --toolkits slack --out ./events.jsonl ``` Filter by toolkit, trigger slug, trigger ID, connected account ID, or userID to focus Claude on a single event source. ## Build on the Composio platform [#build-on-the-composio-platform] Use these commands while building on the Composio developer platform. They initialize local project context, create auth configs, manage connected accounts, test tool execution, inspect logs, and debug trigger flows. ### Initialize project context [#initialize-project-context] ```bash # Initialize local project context composio dev init # Toggle developer mode composio dev --mode on composio dev --mode off # Switch or inspect project scope composio dev projects list composio dev projects switch ``` ### Inspect toolkits and versions [#inspect-toolkits-and-versions] ```bash composio dev toolkits list composio dev toolkits search "email" composio dev toolkits info github composio dev toolkits version github ``` ### Create and inspect auth configs [#create-and-inspect-auth-configs] ```bash # List existing auth configs composio dev auth-configs list composio dev auth-configs list --toolkits github,gmail composio dev auth-configs info ac_xxx # Create an auth config from provider credentials composio dev auth-configs create "GitHub OAuth" \ --toolkit github \ --auth-scheme OAUTH2 \ --scopes "repo,user" \ --custom-credentials '{ "client_id": "...", "client_secret": "..." }' ``` ### Manage connected accounts [#manage-connected-accounts] Top-level `composio link` is the fastest path for personal knowledge work. Use the developer connected-account commands when you're building against project users, auth configs, and playground flows. ```bash composio dev connected-accounts list composio dev connected-accounts list --toolkits github --user-id user_123 composio dev connected-accounts list --status ACTIVE --limit 20 composio dev connected-accounts info ca_xxx composio dev connected-accounts whoami ca_xxx composio dev connected-accounts link ``` ### Execute and inspect logs [#execute-and-inspect-logs] ```bash # Execute a tool through the developer playground path composio dev playground-execute GMAIL_SEND_EMAIL \ -d '{ recipient_email: "you@example.com", subject: "Test", body: "Hello" }' # Inspect tool and trigger logs composio dev logs tools --toolkit gmail --limit 20 composio dev logs tools log_xxx composio dev logs triggers --limit 20 ``` ### Work with triggers [#work-with-triggers] ```bash composio dev triggers list gmail composio dev triggers info GMAIL_NEW_GMAIL_MESSAGE composio dev triggers status composio dev triggers create composio dev triggers enable ti_xxx composio dev listen --trigger-slug GMAIL_NEW_GMAIL_MESSAGE --json --max-events 5 ``` ### Generate type definitions [#generate-type-definitions] For legacy direct tool execution projects, generate local TypeScript or Python types from tool schemas: ```bash composio generate composio generate ts --toolkits github,gmail composio generate py --toolkits github,gmail ``` Reach for this section when you're debugging auth configs, connected accounts, trigger delivery, or tool execution in a Composio project. For user-facing app development, start with the SDK and session docs and keep the CLI as a local debugging companion. ## Building on top of the CLI [#building-on-top-of-the-cli] > Don't build a production integration on top of the CLI. It's in constant development, and Composio doesn't offer CLI-level SLAs as an application runtime contract. For a stable integration, build on the Composio SDKs and APIs instead. That said, the CLI works well as a bootstrap or helper layer for agent-native products: * Use `composio connections list` to inspect which connected accounts are available locally. * Use `composio run` or `composio proxy` for internal automations where CLI churn is acceptable. For an example of a product built around CLI-driven agent workflows, see [Houston](https://github.com/gethouston/houston). ## Help [#help] Use `--help` on the root command or any subcommand: ```bash composio --help composio --help full composio execute --help full composio run --help full composio dev --help full ``` --- # Composio Connect (/docs/composio-connect) Use Composio Connect when you already have an MCP-compatible client and want the shared Composio MCP URL, without creating an SDK session. Connect it to `https://connect.composio.dev/mcp`. If you use Codex or Claude Code and did not explicitly choose MCP, install the native [Composio agent plugin](/docs/agent-plugins) instead. The plugin uses the Composio CLI and is the shortest path for those agents. If you are building an application, start with the [SDK Quickstart](/docs/quickstart) or create a [session MCP endpoint](/docs/sessions-via-mcp) instead. ## How Composio Connect works [#how-composio-connect-works] Composio Connect is an MCP server at `https://connect.composio.dev/mcp` that gives your AI agent access to 1000+ apps, including Gmail, Notion, Slack, GitHub, Linear, HubSpot, and Strava, through a single connection. Rather than exposing every app tool directly, Composio exposes **7 meta-tools** that let the agent discover what's available, authorize apps on demand, and execute tools across apps in parallel. The first time your agent needs an app, Composio generates an OAuth link you approve in your browser; after that the connection persists across sessions. See [Available MCP tools](#available-mcp-tools) for the full list. To get started, pick your client below. ## Claude Code #### Ask Claude Code to install Composio Paste this prompt into Claude Code: ``` Install the Composio CLI: curl -fsSL https://composio.dev/install | sh, then run composio login. ``` ## Claude Cowork (Claude Desktop) #### Open the Connectors menu Click the **+** button in the message box, then choose **Connectors > Add connector > Add custom connector**. #### Add the Composio MCP server Name it **Composio**, paste `https://connect.composio.dev/mcp`, then click **Add**. #### Authorize in your browser Claude opens a browser window. Sign in to authorize Composio. ## ChatGPT #### Enable Developer mode In ChatGPT, open **Settings > Security and login**, then turn on **Developer mode**. This requires ChatGPT Plus, Pro, Business, Enterprise, or Edu. #### Add the MCP server On the **Plugins** page, click **+**, choose **New Plugin**, paste `https://connect.composio.dev/mcp` into **Server URL**, then click **Create**. #### Authorize in your browser Sign in in the browser window ChatGPT opens. #### Enable Composio in a chat For each new chat, click **+**, choose **More**, then select **Composio** to enable its tools. ## Cursor #### Install the Composio plugin Open the [Composio plugin in the Cursor marketplace](https://cursor.com/marketplace/composio), click **Install Composio Plugin for Cursor**, and authorize in your browser. ## OpenClaw #### Ask OpenClaw to install Composio ``` Install the Composio CLI: curl -fsSL https://composio.dev/install | sh, then run composio login. ``` ## Hermes #### Ask Hermes to install Composio ``` Install the Composio CLI: curl -fsSL https://composio.dev/install | sh, then run composio login. ``` ## Notion #### Create a custom agent In Notion's AI agent builder, click **Create Blank**. #### Add the Composio connection Choose **Add Connection > Custom MCP**, enter `https://connect.composio.dev/mcp`, name the connection **Composio**, and complete the OAuth authorization in your browser. ## Codex #### Ask Codex to install Composio ``` Install the Composio CLI: curl -fsSL https://composio.dev/install | sh, then run composio login. ``` ## Warp #### Install from the Warp marketplace Open `warp://settings/mcp?autoinstall=composio` in Warp to install the MCP server, then authorize in your browser. If Warp does not open, add it under **Settings > Agents > MCP servers**. ## Grok #### Open Grok connectors Go to [Grok Connectors](https://grok.com/connectors), click **New Connector**, then choose **Custom**. Custom connectors require a paid tier. #### Add Composio and authorize Paste `https://connect.composio.dev/mcp`, save, and authorize Composio in the sign-in window. ## Gemini CLI #### Ask Gemini CLI to install Composio ``` Install the Composio CLI: curl -fsSL https://composio.dev/install | sh, then run composio login. ``` ## VS Code #### Install from the GitHub MCP registry Open [Composio in the GitHub MCP registry](https://github.com/mcp/ComposioHQ/composio), click **Install in VS Code**, and authorize when prompted. ## Devin Desktop (Windsurf) #### Install Composio in one click Open `windsurf://windsurf-mcp-registry?serverName=composio` in Devin Desktop to install Composio. Your team needs MCP access enabled. #### Or configure it manually Open `~/.codeium/windsurf/mcp_config.json` or **Settings > MCP Configuration**, then add: ```json title="mcp_config.json" { "mcpServers": { "composio": { "serverUrl": "https://connect.composio.dev/mcp" } } } ``` Restart Devin Desktop and click **Authorize** next to Composio. ## Antigravity #### Open your MCP config In Antigravity, choose **Settings > Customizations > Open MCP Config**. #### Add the Composio server Antigravity uses `serverUrl` for remote HTTP servers: ```json title="mcp_config.json" { "mcpServers": { "composio": { "serverUrl": "https://connect.composio.dev/mcp", "headers": { "x-consumer-api-key": "YOUR_CONSUMER_KEY" } } } } ``` Save the file, then refresh the **Installed MCP Servers** list. ## OpenAI Agent Builder (Agent Builder) #### Open Agent Builder Open [OpenAI Agent Builder](https://platform.openai.com/agent-builder) and create a new agent. #### Add Composio as an MCP server In the sidebar, choose **MCP > + Server**. Paste `https://connect.composio.dev/mcp`, choose **Custom headers**, then set `x-consumer-api-key` to `YOUR_CONSUMER_KEY`. ## n8n #### Add an MCP Client node In your n8n workflow, add an **MCP Client** node or MCP Client Tool sub-node. #### Configure Composio Set the connection type to **HTTP Streamable** and URL to `https://connect.composio.dev/mcp`. Select **Header Auth**, create a credential with header name `x-consumer-api-key`, and use `YOUR_CONSUMER_KEY` as its value. ## Generic MCP URL #### Add the Composio server Use `https://connect.composio.dev/mcp` with streamable HTTP and this header: ```json { "mcpServers": { "composio": { "url": "https://connect.composio.dev/mcp", "headers": { "x-consumer-api-key": "YOUR_CONSUMER_KEY" } } } } ``` ## Connect your apps [#connect-your-apps] Your agent will prompt you to connect apps when needed. If you want to connect an app ahead of time, ask your agent to start the connection and complete the OAuth flow it opens. ## Available MCP tools [#available-mcp-tools] Composio Connect exposes 7 meta-tools that orchestrate access to all supported apps. Your agent uses these to discover, connect, and execute upstream tools — you don't need to call them directly. * **`COMPOSIO_SEARCH_TOOLS`** — Search the Composio catalog and return relevant tools for a user request, along with a suggested execution plan. * **`COMPOSIO_GET_TOOL_SCHEMAS`** — Fetch full input schemas for tool slugs returned by search. * **`COMPOSIO_MULTI_EXECUTE_TOOL`** — Execute one or more discovered tools in parallel across connected apps (up to 50 per call). * **`COMPOSIO_MANAGE_CONNECTIONS`** — Create, list, rename, or remove OAuth connections to upstream apps. * **`COMPOSIO_WAIT_FOR_CONNECTIONS`** — Wait for a user to complete an OAuth flow before the agent continues. * **`COMPOSIO_REMOTE_WORKBENCH`** — Run Python in a remote sandbox for bulk operations or processing large tool responses. * **`COMPOSIO_REMOTE_BASH_TOOL`** — Run bash in a remote sandbox for file processing and large data handling. ## Troubleshooting [#troubleshooting] ### Tools aren't appearing in my agent [#tools-arent-appearing-in-my-agent] 1. Confirm the MCP server is connected. In Claude Desktop, go to **Settings > Connectors** and check that Composio shows a `CUSTOM` badge. In Claude Code, run `/mcp` and confirm Composio is enabled. 2. Clear the connector cache. In Claude Desktop: click the **⋮** next to Composio and select **Clear cache**. 3. If the issue persists, disconnect and re-add the connector: * **Claude Desktop** — click **⋮ > Disconnect**, then **Remove**. Re-add via **Add custom connector**. * **Claude Code** — re-run the setup command from the Claude Code section above. ### The OAuth link expired or didn't open [#the-oauth-link-expired-or-didnt-open] OAuth links are short-lived. If the browser window doesn't open or the link has expired, ask your agent to retry the action — Composio will generate a fresh link. ### An app action is failing with an auth error [#an-app-action-is-failing-with-an-auth-error] 1. Ask your agent to inspect the app connection. 2. If the connection is unhealthy, disconnect and reconnect it when prompted. 3. Retry the action. ### I want to remove or reconnect an app [#i-want-to-remove-or-reconnect-an-app] Ask your agent to manage the connection. It can prompt you to disconnect, delete, or re-authorize an app. ### I still need help [#i-still-need-help] Reach out at [support@composio.dev](mailto:support@composio.dev) or join the [Composio Discord](https://discord.com/invite/cNruWaAhQk). --- # What is a session? (/docs/how-composio-works) A **session** is the runtime context for an agentic run: the scoped environment an AI agent works in while it acts for one of your users. You create it with `composio.create(userId)`, and it ties together the user, the toolkits available, authentication, and connected accounts. By default it gives the agent meta tools to discover, authenticate, and execute app tools at runtime, instead of loading hundreds of tool definitions into context. ## The basics [#the-basics] Create a session for a user, then read its tools formatted for your framework. To connect over MCP instead, see [Using sessions via MCP](/docs/sessions-via-mcp). **Python:** ```python session = composio.sessions.create(user_id="user_123") tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const tools = await session.tools(); ``` A session scopes four things: * **userID**: whose connected accounts and tool executions are in scope. * **Tool access**: all toolkits by default, or a filtered set of toolkits, tools, or tags. * **Authentication**: managed auth, custom auth configs, and connected-account selection. * **Execution state**: logs, tool memory, MCP state, and workbench files for the task. ## Tools and toolkits [#tools-and-toolkits] A **toolkit** is a collection of related tools for a service. The `github` toolkit, for example, contains tools for creating issues, managing pull requests, and starring repositories. A **tool** is an individual action your agent can execute. Each tool has an input schema (its parameters) and an output schema (what it returns), and follows a `{TOOLKIT}_{ACTION}` naming pattern, like `GITHUB_CREATE_ISSUE`. Every toolkit in the catalog is discoverable by default. Create a session without a `toolkits` parameter and the agent can find any of them at runtime. To restrict the set, pass `toolkits` when you create the session. See [Enable and disable toolkits](/docs/configuring-sessions). You can also bind local, in-process tools to a session with the experimental [custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits) API. ## Users [#users] A user is an identifier from your app. Composio stores connections under that ID, so tools run with the right account and stay isolated from other users. Use a stable identifier like your database ID, never one that can change. **userID best practices** * **Recommended:** database UUID or primary key (`user.id`) * **Acceptable:** unique username (`user.username`) * **Avoid:** email addresses (they can change) * **Never:** `default` in production (it exposes other users' data) A user can connect multiple accounts for the same toolkit, like work and personal Gmail. Use the same userID, then select the connected account when a session needs a specific one. See [Managing multiple connected accounts](/docs/authentication/managing-multiple-connected-accounts). ## Meta tools [#meta-tools] A session gives your agent meta tools, a small fixed set that discover, authenticate, and execute tools at runtime, so you never load hundreds of tool definitions into context: The agent searches for relevant tools, authenticates if needed, and executes them through the same session. Meta-tool calls share context, so the agent searches in one call and executes in the next without losing state. See the [Meta Tools reference](/toolkits/meta-tools) for each tool's input and output schema. Know the exact tools upfront? The [direct tools preset](/docs/configuring-sessions#direct-tools-preset) returns them directly from `session.tools()` with no search step, while keeping session auth, connected accounts, and the workbench. ## Executing session tools [#executing-session-tools] When your model asks to call a session tool, execute it with `session.execute()`. It routes the call through the session's tool router, which is the only place session meta-tools run: **Python:** ```python result = session.execute("COMPOSIO_SEARCH_TOOLS", arguments={"queries": [{"use_case": "send an email"}]}) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const result = await session.execute("COMPOSIO_SEARCH_TOOLS", { queries: [{ use_case: "send an email" }] }); ``` Do not execute session tools with `tools.execute()`, or by passing a user ID to a provider's `handle_tool_calls` helper — both take the direct execution path, which does not carry your session, and meta-tools fail there with `"can only be called inside a tool-router session"`. The [OpenAI](/docs/providers/openai) and [Anthropic](/docs/providers/anthropic) helpers also accept the session itself (`handle_tool_calls(response=response, session=session)` in Python, `handleToolCalls(session, response)` in TypeScript), which routes through the session while keeping provider argument normalization. ## Authentication [#authentication] When a tool needs a connection, the session generates a Connect Link with `session.authorize()`, or the agent handles the flow through `COMPOSIO_MANAGE_CONNECTIONS`. In chat, the agent can pause, ask the user to connect an app, then retry the tool once auth completes. Composio manages the OAuth redirects, token exchange, and refresh. Once a user connects a toolkit, the connected account persists and future sessions reuse it without re-authentication. For OAuth toolkits, Composio uses [managed apps](/docs/authentication/custom-app-vs-managed-app) by default. Bring your own app when you need your own branding, scopes, or consent screen. ## Sandbox [#sandbox] Handle large responses and bulk operations in the remote sandbox. Instead of stuffing long tool responses into the model context, the agent reads files, searches outputs, writes Python, transforms data, and calls Composio tools in bulk. The sandbox is scoped to the session, so files, variables, helper functions, and intermediate results stay available while the agent works through a task. ## How sessions behave [#how-sessions-behave] Every `create()` call returns a new session ID. Use it for a fresh task context. Sessions persist on the server and don't expire. For multi-turn conversations, store the session ID and reuse it with `composio.use()` instead of calling `create()` again. **Python:** ```python session = composio.use("session_id") tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.use("session_id"); const tools = await session.tools(); ``` You can also update a session in place instead of creating a new one: **Python:** ```python session.update( toolkits=["gmail", "slack"], auth_configs={"gmail": "ac_new_config"}, connected_accounts={"slack": ["ca_work_slack"]}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.use("session_id"); await session.update({ toolkits: ["gmail", "slack"], authConfigs: { gmail: "ac_new_config" }, connectedAccounts: { slack: ["ca_work_slack"] }, }); ``` Create a new session for a different user or a fundamentally different task setup. Reuse or update a session when the same conversation should keep its tool, auth, and workbench context. ## Next [#next] - [Configuring Sessions](/docs/configuring-sessions): Enable toolkits, set auth configs, and select connected accounts --- # Configuring Sessions (/docs/configuring-sessions) ## Creating a session [#creating-a-session] **Python:** ```python session = composio.sessions.create(user_id="user_123") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); ``` By default, a session has access to every toolkit in the Composio catalog. Your agent can discover and use any of them through `COMPOSIO_SEARCH_TOOLS`. Use the options below to restrict or customize what's available. You can also attach local custom tools and custom toolkits that run in-process alongside Composio tools. See [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits). ## Enabling toolkits [#enabling-toolkits] To limit a session to specific toolkits, pass an array of toolkit slugs. The agent can only discover and use tools from these toolkits. **Python:** ```python # Using array format session = composio.sessions.create( user_id="user_123", toolkits=["github", "gmail", "slack"] ) # Using object format with enable key session = composio.sessions.create( user_id="user_123", toolkits={"enable": ["github", "gmail", "slack"]} ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Using array format const session = await composio.create("user_123", { toolkits: ["github", "gmail", "slack"], }); // Using object format with enable key const session2 = await composio.create("user_123", { toolkits: { enable: ["github", "gmail", "slack"] }, }); ``` ## Disabling toolkits [#disabling-toolkits] To keep every toolkit discoverable except a few, use the `disable` syntax. This is useful when you want broad access but need to exclude specific toolkits. **Python:** ```python session = composio.sessions.create( user_id="user_123", toolkits={"disable": ["exa", "firecrawl"]} ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { toolkits: { disable: ["exa", "firecrawl"] }, }); ``` ## Direct tools preset [#direct-tools-preset] The direct tools preset preloads every tool allowed by session filters into the session's tool list and disables session meta tools by default. Use it for specialized agents with a narrow tool set that don't need dynamic tool discovery, in-chat auth, or workbench helpers. This is not the default mode for broad agents. The default session behavior keeps meta tools available so the agent can search for relevant tools and avoid context bloat. **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], tools={ "gmail": { "enable": [ "GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT", ], }, }, session_preset=SESSION_PRESET_DIRECT_TOOLS, ) tools = session.tools() print([tool.name for tool in tools]) # GMAIL_FETCH_EMAILS # GMAIL_CREATE_EMAIL_DRAFT ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from '@composio/core'; import { OpenAIAgentsProvider } from '@composio/openai-agents'; const composio = new Composio({ apiKey: 'your_api_key', provider: new OpenAIAgentsProvider(), }); const session = await composio.create("user_123", { toolkits: ["gmail"], tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"], }, }, sessionPreset: SessionPreset.DIRECT_TOOLS, }); const tools = await session.tools(); console.log(tools.map((tool) => tool.name)); // GMAIL_FETCH_EMAILS // GMAIL_CREATE_EMAIL_DRAFT ``` ### Enable selected meta tools [#enable-selected-meta-tools] With the direct tools preset, you can re-enable supported meta tool groups that your agent still needs. This session loads Gmail tools upfront while keeping connection management and workbench support available: **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], tools={ "gmail": { "enable": [ "GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT", ], }, }, session_preset=SESSION_PRESET_DIRECT_TOOLS, manage_connections={ "enable": True, }, sandbox={ "enable": True, }, ) tools = session.tools() print([tool.name for tool in tools]) # GMAIL_FETCH_EMAILS # GMAIL_CREATE_EMAIL_DRAFT # COMPOSIO_MANAGE_CONNECTIONS # COMPOSIO_REMOTE_WORKBENCH # COMPOSIO_REMOTE_BASH_TOOL ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from '@composio/core'; import { OpenAIAgentsProvider } from '@composio/openai-agents'; const composio = new Composio({ apiKey: 'your_api_key', provider: new OpenAIAgentsProvider(), }); const session = await composio.create("user_123", { toolkits: ["gmail"], tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"], }, }, sessionPreset: SessionPreset.DIRECT_TOOLS, manageConnections: { enable: true, }, sandbox: { enable: true, }, }); const tools = await session.tools(); console.log(tools.map((tool) => tool.name)); // GMAIL_FETCH_EMAILS // GMAIL_CREATE_EMAIL_DRAFT // COMPOSIO_MANAGE_CONNECTIONS // COMPOSIO_REMOTE_WORKBENCH // COMPOSIO_REMOTE_BASH_TOOL ``` ## Enabling or disabling specific tools [#enabling-or-disabling-specific-tools] To control which individual tools are available within a toolkit, use the `tools` configuration. The key is the toolkit slug and the value specifies which tools to enable or disable. To enable only specific tools, pass an `enable` list per toolkit: **Python:** ```python session = composio.sessions.create( user_id="user_123", tools={ # Only these Gmail tools will be available "gmail": {"enable": ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"]}, # Only issue-related GitHub tools "github": {"enable": ["GITHUB_CREATE_ISSUE", "GITHUB_GET_ISSUE"]} } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { tools: { // Only these Gmail tools will be available gmail: { enable: ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"] }, // Only issue-related GitHub tools github: { enable: ["GITHUB_CREATE_ISSUE", "GITHUB_GET_ISSUE"] } } }); ``` The shorthand array syntax is equivalent to `enable`: **Python:** ```python session = composio.sessions.create( user_id="user_123", tools={ "gmail": ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"], "github": ["GITHUB_CREATE_ISSUE", "GITHUB_GET_ISSUE"] } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { tools: { gmail: ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"], github: ["GITHUB_CREATE_ISSUE", "GITHUB_GET_ISSUE"] } }); ``` To keep every tool in a toolkit except a few, use `disable`: **Python:** ```python session = composio.sessions.create( user_id="user_123", tools={ # All Slack tools except delete "slack": {"disable": ["SLACK_DELETE_MESSAGE"]}, # All GitHub tools except destructive ones "github": {"disable": ["GITHUB_DELETE_REPO", "GITHUB_DELETE_BRANCH"]} } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { tools: { // All Slack tools except delete slack: { disable: ["SLACK_DELETE_MESSAGE"] }, // All GitHub tools except destructive ones github: { disable: ["GITHUB_DELETE_REPO", "GITHUB_DELETE_BRANCH"] } } }); ``` ## Filtering tools by tags [#filtering-tools-by-tags] Tools carry behavior tags that you can filter on. The available tags are: | Tag | Description | | ----------------- | ------------------------------------------- | | `readOnlyHint` | Tools that only read data | | `destructiveHint` | Tools that modify or delete data | | `idempotentHint` | Tools that can be safely retried | | `openWorldHint` | Tools that operate in an open world context | To apply tag filters across all toolkits, pass `tags` at the session level: **Python:** ```python # Only include read-only and idempotent tools session = composio.sessions.create( user_id="user_123", tags=["readOnlyHint", "idempotentHint"] ) # Enable some tags, disable others session = composio.sessions.create( user_id="user_123", tags={ "enable": ["readOnlyHint"], "disable": ["destructiveHint"] } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Only include read-only and idempotent tools const session = await composio.create("user_123", { tags: ["readOnlyHint", "idempotentHint"] }); // Enable some tags, disable others const sessionWithTagConfig = await composio.create("user_123", { tags: { enable: ["readOnlyHint"], disable: ["destructiveHint"] } }); ``` To override the global tags for a specific toolkit, set `tags` inside that toolkit's `tools` config: **Python:** ```python session = composio.sessions.create( user_id="user_123", # Global: only read-only tools tags=["readOnlyHint"], tools={ # Override for GitHub: allow all tools except destructive "github": {"tags": {"disable": ["destructiveHint"]}}, # Override for Gmail: only read-only tools (explicit) "gmail": {"tags": ["readOnlyHint"]} } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { // Global: only read-only tools tags: ["readOnlyHint"], tools: { // Override for GitHub: allow all tools except destructive github: { tags: { disable: ["destructiveHint"] } }, // Override for Gmail: only read-only tools (explicit) gmail: { tags: ["readOnlyHint"] } } }); ``` ## Preloading tools [#preloading-tools] Return a known set of tools directly from `session.tools()` and the session MCP tool list, without the agent searching for them first. By default, sessions expose [meta tools](/toolkits/meta-tools) that let the agent discover app tools at runtime. Use `preload.tools` when you already know which tools the agent needs, so it can call them without going through search each time. Keep the preloaded set small, generally fewer than 20 tools, to avoid context bloat. > Requires `@composio/core` ≥ `0.9.0` (TypeScript) or `composio` ≥ `0.13.0` (Python). Older SDKs do not support `preload.tools`, `sessionPreset` / `session_preset`, or custom-tool `preload`. > `preload.tools` is not supported when `multiAccount.enable` is true. See [Managing multiple connected accounts](/docs/authentication/managing-multiple-connected-accounts). **Python:** ```python from composio import Composio from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], preload={ "tools": [ "GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT", ], }, ) tools = session.tools() print([tool.name for tool in tools]) # GMAIL_FETCH_EMAILS # GMAIL_CREATE_EMAIL_DRAFT # COMPOSIO_SEARCH_TOOLS # ... other default meta tools ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; import { OpenAIAgentsProvider } from '@composio/openai-agents'; const composio = new Composio({ apiKey: 'your_api_key', provider: new OpenAIAgentsProvider(), }); const session = await composio.create("user_123", { toolkits: ["gmail"], preload: { tools: ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"], }, }); const tools = await session.tools(); console.log(tools.map((tool) => tool.name)); // GMAIL_FETCH_EMAILS // GMAIL_CREATE_EMAIL_DRAFT // COMPOSIO_SEARCH_TOOLS // ... other default meta tools ``` For SDK custom tools, set `preload: true` on the custom tool or custom toolkit. See [Preloading custom tools](/docs/extending-sessions/custom-tools-and-toolkits#preloading-custom-tools). To preload every tool allowed by the session filters, use the `preload.tools = "all"` shortcut (`preload={"tools": "all"}` in Python, `preload: { tools: "all" }` in TypeScript). The `all` shorthand works for both Composio tools and SDK custom tools. ## Custom auth configs [#custom-auth-configs] Use your own OAuth credentials instead of Composio's defaults. Pass an auth config ID per toolkit: **Python:** ```python session = composio.sessions.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", "slack": "ac_your_slack_config" } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { authConfigs: { github: "ac_your_github_config", slack: "ac_your_slack_config", }, }); ``` See [White-labeling authentication](/docs/authentication/white-labeling-authentication) for branding, or [Managed vs custom auth](/docs/authentication/custom-app-vs-managed-app) for toolkits that require your own credentials. ## Account selection [#account-selection] When a user has multiple connected accounts for the same toolkit, specify which one the session uses: **Python:** ```python session = composio.sessions.create( user_id="user_123", connected_accounts={ "gmail": ["ca_work_gmail"], "github": ["ca_personal_github"], } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { connectedAccounts: { gmail: ["ca_work_gmail"], github: ["ca_personal_github"], }, }); ``` > Arrays are the preferred format for `connectedAccounts`. A single string (e.g. `"ca_work_gmail"`) is still accepted for backwards compatibility and is automatically coerced to a single-element array. Only one account per toolkit is allowed when [multi-account mode](/docs/authentication/managing-multiple-connected-accounts) is disabled. ### Precedence [#precedence] When executing a tool, the session selects the connected account in this order: 1. The `connectedAccounts` override, if provided in the session config. 2. The `authConfigs` override, which finds or creates a connection on that config. 3. An auth config previously created for this toolkit. 4. A new auth config created using Composio managed auth. 5. Otherwise, an error if no Composio managed auth scheme exists for the toolkit. When a user has multiple connected accounts for a toolkit, the session uses the most recently connected one. ## Disabling the sandbox [#disabling-the-sandbox] By default, sessions include the [sandbox](/docs/sandbox/remote), a persistent environment that provides `COMPOSIO_REMOTE_WORKBENCH` and `COMPOSIO_REMOTE_BASH_TOOL`. If your use case doesn't need code execution, disable it: **Python:** ```python session = composio.sessions.create( user_id="user_123", sandbox={ "enable": False } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { sandbox: { enable: false, }, }); ``` When disabled: * `COMPOSIO_REMOTE_WORKBENCH` and `COMPOSIO_REMOTE_BASH_TOOL` are excluded from the session * Sandbox-related system prompt lines are stripped * Direct sandbox calls are rejected with a 400 error > `sandbox` is the preferred config key. `workbench` still works as a fully supported alias and isn't deprecated, so existing code keeps running unchanged. ## Sandbox compute tier [#sandbox-compute-tier] The sandbox runs per session. Pick a compute tier to match the workload: heavier code execution or larger in-memory data benefits from a bigger sandbox. Pass the tier via `sandbox.sandbox_size` (snake\_case on the wire, `sandboxSize` in the TypeScript SDK). > Requires `@composio/core` ≥ `0.8.1` (TypeScript) or `composio` ≥ `0.12.1` (Python). Older SDKs reject `sandboxSize` (TypeScript) or silently drop `sandbox_size` (Python). See the [release notes](/docs/changelog/2026/04/28). | Tier | vCPU | RAM | | ---------- | ---- | ---- | | `standard` | 1 | 1 GB | | `medium` | 2 | 2 GB | | `large` | 4 | 4 GB | | `xlarge` | 8 | 8 GB | Defaults to `standard` when omitted. **Python:** ```python session = composio.sessions.create( user_id="user_123", sandbox={ "sandbox_size": "large", }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { sandbox: { enable: true, sandboxSize: "large", }, }); ``` > **Pricing:** Sandboxes are not billed today. Composio plans to begin billing for sandbox usage soon (metered by tier and runtime). Pick a tier that matches your workload, but expect future pricing to track actual usage. Changing `sandbox_size` on an existing session recreates the sandbox on the next access. The sandbox's in-memory filesystem state is lost, but the persistent [`/mnt/files/` mount](/docs/sandbox/remote#files-and-mounts) survives the restart. ## Session methods [#session-methods] For framework examples, see provider-specific documentation like [OpenAI](/docs/providers/openai) or [Vercel AI SDK](/docs/providers/vercel). To connect over MCP instead, see [Using sessions via MCP](/docs/sessions-via-mcp). ### tools() [#tools] Get the tools the session exposes for your AI framework. By default these are the session's [meta tools](/toolkits/meta-tools), formatted for your configured provider. **Python:** ```python tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const tools = await session.tools(); ``` ### authorize() [#authorize] Manually authenticate a user to a toolkit outside of the chat flow. **Python:** ```python connection_request = session.authorize("github") print(connection_request.redirect_url) connected_account = connection_request.wait_for_connection() ``` **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("github", { callbackUrl: "https://myapp.com/callback", }); console.log(connectionRequest.redirectUrl); const connectedAccount = await connectionRequest.waitForConnection(); ``` For more details, see [Manually authenticating users](/docs/authentication/manually-authenticating). ### toolkits() [#toolkits] List the toolkits enabled for the session and their connection status, sorted by popularity. Use it to build a UI showing which apps are connected. Each toolkit includes its `slug`, `name`, `logo`, and connection status, and the call returns the first 20 by default. **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"}`); }); ``` Filter to only connected toolkits: **Python:** ```python connected = session.toolkits(is_connected=True) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const connected = await session.toolkits({ isConnected: true }); ``` Paginate through every toolkit with `limit` and the returned cursor: **Python:** ```python all_toolkits = [] cursor = None while True: result = session.toolkits(limit=20, next_cursor=cursor) all_toolkits.extend(result.items) cursor = result.next_cursor if not cursor: break ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const allToolkits: any[] = []; let cursor: string | undefined; do { const { items, cursor: nextCursor } = await session.toolkits({ limit: 20, cursor }); allToolkits.push(...items); cursor = nextCursor; } while (cursor); ``` ### delete() [#delete] Delete a session when you're done with it. Deleted sessions immediately stop being retrievable or executable, and the call returns the deleted `session_id`. Deleting a missing or already-deleted session surfaces the backend `404`. > Requires `@composio/core` ≥ `0.13.1` (TypeScript) or `composio` ≥ `0.17.1` (Python). **Python:** ```python result = session.delete() print(result["session_id"], result["deleted"]) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create('user_123'); const result = await session.delete(); console.log(result.sessionId, result.deleted); ``` ## Browsing the catalog [#browsing-the-catalog] Before configuring a session, explore the toolkits and tools available. Browse them visually at [dashboard.composio.dev](https://dashboard.composio.dev?utm_source=docs\&utm_medium=content\&utm_campaign=docs-configuring-sessions) or in the [docs catalog](/toolkits), or fetch them programmatically: **Python:** ```python # List toolkits toolkits = composio.toolkits.get() # List tools within a toolkit (top 20 by default) tools = composio.tools.get("user_123", toolkits=["GITHUB"]) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const userId = 'user_123'; // List toolkits const toolkits = await composio.toolkits.get(); // List tools within a toolkit (top 20 by default) const tools = await composio.tools.get(userId, { toolkits: ["GITHUB"] }); ``` Inspect a tool's input and output schema without a user context with `getRawComposioToolBySlug`: **Python:** ```python tool = composio.tools.get_raw_composio_tool_by_slug("GMAIL_SEND_EMAIL") print(tool.name) print(tool.description) print(tool.input_parameters) print(tool.output_parameters) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const tool = await composio.tools.getRawComposioToolBySlug("GMAIL_SEND_EMAIL"); console.log(tool.name); console.log(tool.description); console.log(tool.inputParameters); console.log(tool.outputParameters); ``` ## Next [#next] - [Sandbox](/docs/sandbox/remote): Give sessions a persistent compute environment with COMPOSIO_REMOTE_WORKBENCH and COMPOSIO_REMOTE_BASH_TOOL --- # Authentication (/docs/authentication) Composio organizes everything around your **users**. A user is whoever your agent acts on behalf of: a person in your app, identified by a [`userID`](/docs/how-composio-works) you choose. Authentication is always per user. Each user connects their own accounts, their Gmail, their GitHub, their Slack, and Composio stores and refreshes those credentials against that `userID`. This is the core idea: your agent runs the same tools for many people, and every tool call runs as a specific user against that user's connected accounts. User A's agent never touches user B's data. You pass the `userID` when you create a session, and Composio handles the auth from there. Because connections are stored under the `userID`, use a stable identifier, like your database ID, never one that can change. **userID best practices** * **Recommended:** database UUID or primary key (`user.id`) * **Acceptable:** unique username (`user.username`) * **Avoid:** email addresses (they can change) * **Never:** `default` in production (it exposes other users' data) Your users connect their accounts through a secure [Connect Link](/reference/api-reference/connected-accounts/postConnectedAccountsLink), and Composio manages their tokens for you. ## How Composio handles authentication [#how-composio-handles-authentication] Every session includes the [`COMPOSIO_MANAGE_CONNECTIONS`](/toolkits/meta-tools/manage_connections) meta tool. When a tool needs an account, it reads the toolkit's **auth config** (how that toolkit authenticates: method, scopes, credentials), creates a connection, and returns a secure [Connect Link](/reference/api-reference/connected-accounts/postConnectedAccountsLink). This works for all Composio managed connections, so you don't have to set up any OAuth credentials yourself. The user signs in on the hosted link and Composio stores the resulting connected account. Credentials never pass through your app or the model, so it's safe to surface the link right in the chat. You only need a [custom auth config](/docs/authentication/custom-app-vs-managed-app) to bring your own OAuth app, request specific scopes, or use a toolkit without managed auth. ## In-chat authentication [#in-chat-authentication] You can also call `COMPOSIO_MANAGE_CONNECTIONS` yourself, intercept the Connect Link, and surface it wherever you need: DM it to the user, render it in your own UI, or email it. See [redirect auth links](/examples/general-agent-with-pi#redirect-auth-links) for a worked example. ### Custom callback URL [#custom-callback-url] To send users back to your app after they connect, pass a `callback_url`: **Python:** ```python session = composio.create( user_id="user_123", manage_connections={"callback_url": "https://yourapp.com/chat"}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { manageConnections: { callbackUrl: "https://yourapp.com/chat" }, }); ``` ## Manually triggering authentication [#manually-triggering-authentication] Don't want to wait for the agent? Call `session.authorize()` to generate a Connect Link on demand, for onboarding, a settings page, or a pre-flight check before a task. - [Manual auth management](/docs/authentication/manually-authenticating): Generate Connect Links yourself, check connection status, and disable in-chat prompts. - [Multiple connected accounts](/docs/authentication/managing-multiple-connected-accounts): Let one user choose between work, personal, or other accounts. - [Shared connections](/docs/extending-sessions/shared-connections): Share one connected account with a controlled set of users. - [Import existing connections](/docs/authentication/importing-existing-connections): Bring credentials your application already stores into Composio. - [Managed vs custom auth](/docs/authentication/custom-app-vs-managed-app): Decide whether to use Composio credentials or your own OAuth app. - [Programmatic auth configs](/docs/authentication/programmatic-auth-configs): Create auth configs in code and attach them to sessions. - [Control OAuth scopes](/docs/authentication/controlling-scopes): Choose the permissions requested when a user connects. - [White-label authentication](/docs/authentication/white-labeling-authentication): Use your own OAuth app and remove Composio branding. --- # 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. ## 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 ``` ## 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 --- # Managing multiple connected accounts (/docs/authentication/managing-multiple-connected-accounts) Users can connect multiple accounts for the same toolkit (e.g., personal and work Gmail accounts). This guide covers how to enable multi-account mode, label accounts with aliases, and select which account to use. ## Multi-account mode [#multi-account-mode] By default, each session uses **one account per toolkit**. Enable multi-account mode to let users connect and use multiple accounts for the same toolkit within a single session. **Python:** ```python session = composio.create( user_id="user_123", toolkits=["gmail"], multi_account={ "enable": True, "max_accounts_per_toolkit": 3, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { toolkits: ["gmail"], multiAccount: { enable: true, maxAccountsPerToolkit: 3, }, }); ``` ### Configuration options [#configuration-options] | Option (TS / Python) | Type | Default | Description | | --------------------------------------------------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enable` | `boolean` | `false` | Enable multi-account mode for this session | | `maxAccountsPerToolkit` / `max_accounts_per_toolkit` | `number` | `5` | Maximum connected accounts per toolkit (2-10) | | `requireExplicitSelection` / `require_explicit_selection` | `boolean` | `false` | When true and a toolkit has multiple active connected accounts, the agent must provide the `account` parameter in the tool execution call to select which account to use. `account` can be either a connected account ID or an alias (see the Aliases section below). When false, the default account (most recently connected active account) is used automatically | When multi-account mode is disabled (the default), each session uses the most recently connected account for each toolkit. ## Connecting multiple accounts [#connecting-multiple-accounts] Call `session.authorize()` multiple times for the same toolkit. Each call creates a separate connected account. **Python:** ```python session = composio.create(user_id="user_123", multi_account={"enable": True}) # Connect work account work_auth = session.authorize("gmail", alias="work-gmail") print(f"Connect work Gmail: {work_auth.redirect_url}") work_connection = work_auth.wait_for_connection() # Connect personal account personal_auth = session.authorize("gmail", alias="personal-gmail") print(f"Connect personal Gmail: {personal_auth.redirect_url}") personal_connection = personal_auth.wait_for_connection() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { toolkits: ["gmail"], multiAccount: { enable: true }, }); // Connect work account const workAuth = await session.authorize("gmail", { alias: "work-gmail" }); console.log(`Connect work Gmail: ${workAuth.redirectUrl}`); const workConnection = await workAuth.waitForConnection(); // Connect personal account const personalAuth = await session.authorize("gmail", { alias: "personal-gmail" }); console.log(`Connect personal Gmail: ${personalAuth.redirectUrl}`); const personalConnection = await personalAuth.waitForConnection(); ``` ## Aliases [#aliases] Aliases are human-readable labels for connected accounts (e.g., `"work-gmail"`, `"personal-github"`). They make it easier for agents and users to identify which account is which. * Must be unique per user and toolkit within a project * Can be set during connection or updated after ### Setting an alias during connection [#setting-an-alias-during-connection] Pass `alias` to `session.authorize()`: **Python:** ```python session = composio.create(user_id="user_123") connection_request = session.authorize("gmail", alias="work-gmail") ``` **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", { alias: "work-gmail" }); ``` The direct-execution methods `connectedAccounts.initiate()` and `connectedAccounts.link()` accept the same `alias` parameter. To create another active connection for the same user and auth config, also pass `allow_multiple=True` in Python or `allowMultiple: true` in TypeScript. ### Updating or clearing an alias [#updating-or-clearing-an-alias] **Python:** ```python # Set or update an alias composio.connected_accounts.update("ca_abc123", alias="work-gmail") # Clear an alias composio.connected_accounts.update("ca_abc123", alias="") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Set or update an alias await composio.connectedAccounts.update("ca_abc123", { alias: "work-gmail" }); // Clear an alias await composio.connectedAccounts.update("ca_abc123", { alias: "" }); ``` ## Selecting a specific account for a session [#selecting-a-specific-account-for-a-session] Pin a session to specific accounts by passing their IDs in the session config. To retrieve connected account IDs, see [List accounts](/docs/auth-configuration/connected-accounts#list-accounts). **Python:** ```python session = composio.create( user_id="user_123", connected_accounts={ "gmail": ["ca_work_gmail_id"], "github": ["ca_personal_github_id"], }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { connectedAccounts: { gmail: ["ca_work_gmail_id"], github: ["ca_personal_github_id"], }, }); ``` ## Viewing session's active accounts [#viewing-sessions-active-accounts] Use `session.toolkits()` to see which accounts are currently active: **Python:** ```python toolkits = session.toolkits() for toolkit in toolkits.items: if toolkit.connection and toolkit.connection.connected_account: print(f"{toolkit.name}: {toolkit.connection.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 toolkits = await session.toolkits(); for (const toolkit of toolkits.items) { if (toolkit.connection?.connectedAccount) { console.log(`${toolkit.name}: ${toolkit.connection.connectedAccount.id}`); } } ``` ## Next [#next] - [Shared connections](/docs/extending-sessions/shared-connections): Make one connected account usable by multiple users with a per-user access control list --- # Importing existing connections (/docs/authentication/importing-existing-connections) If your users have already authenticated with a service and you have their credentials (API keys, bearer tokens, etc.), you can pass those directly into Composio. No re-authentication required. This is useful when: * Your app already stores API keys or tokens for users * You're adopting Composio and want to onboard existing users without disrupting them * You want to use bearer tokens with OAuth toolkits (Gmail, GitHub, Slack, etc.) without setting up an OAuth app ## How it works [#how-it-works] ## Prerequisites [#prerequisites] 1. **An [auth config](/docs/authentication/programmatic-auth-configs)** for the toolkit you're importing into 2. **The existing credentials** for each user (API keys, bearer tokens, username/password, etc.) 3. **A userID** for each user. Any string that uniquely identifies them in your system. ## API keys [#api-keys] For services that use API key authentication (e.g., SendGrid, Tavily, PostHog): **Python:** ```python from composio import Composio from composio.types import auth_scheme composio = Composio(api_key="your-api-key") connection = composio.connected_accounts.initiate( user_id="user_123", auth_config_id="ac_your_auth_config", config=auth_scheme.api_key({ "api_key": "sg-existing-sendgrid-key", }), ) # API key connections are immediately active print(f"Connected: {connection.id}") ``` **TypeScript:** ```typescript import { Composio, AuthScheme } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); const connection = await composio.connectedAccounts.initiate( 'user_123', 'ac_your_auth_config', { config: AuthScheme.APIKey({ api_key: 'sg-existing-sendgrid-key', }), } ); // API key connections are immediately active console.log('Connected:', connection.id); ``` ## Bearer tokens [#bearer-tokens] If you manage your own OAuth flow and already have an access token for a service, you can import it into Composio as a bearer token. This lets you bring existing OAuth connections into Composio without re-authenticating your users. It works with **all toolkits that support OAuth2 or S2S auth** (Gmail, GitHub, Slack, Google Docs, and more). Any additional parameters the toolkit supports (e.g., `subdomain`, `base_url`) work the same way. Since you're providing your own token, Composio won't handle OAuth refresh. You're responsible for refreshing the token on your end and pushing the updated value to Composio via the [PATCH method](#updating-credentials) whenever it changes. After [creating an auth config](/docs/authentication/programmatic-auth-configs) with `authScheme: "BEARER_TOKEN"`, use the snippet below to create a connected account: **Python:** ```python from composio import Composio from composio.types import auth_scheme composio = Composio(api_key="your-api-key") connection = composio.connected_accounts.initiate( user_id="user_123", auth_config_id="ac_your_auth_config", config=auth_scheme.bearer_token({ "token": "existing-bearer-token", }), ) # Bearer token connections are immediately active print(f"Connected: {connection.id}") ``` **TypeScript:** ```typescript import { Composio, AuthScheme } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); const connection = await composio.connectedAccounts.initiate( 'user_123', 'ac_your_auth_config', { config: AuthScheme.BearerToken({ token: 'existing-bearer-token', }), } ); // Bearer token connections are immediately active console.log('Connected:', connection.id); ``` ## Basic auth [#basic-auth] **Python:** ```python from composio import Composio from composio.types import auth_scheme composio = Composio(api_key="your-api-key") connection = composio.connected_accounts.initiate( user_id="user_123", auth_config_id="ac_your_auth_config", config=auth_scheme.basic({ "username": "user@example.com", "password": "existing-password", }), ) # Basic auth connections are immediately active print(f"Connected: {connection.id}") ``` **TypeScript:** ```typescript import { Composio, AuthScheme } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); const connection = await composio.connectedAccounts.initiate( 'user_123', 'ac_your_auth_config', { config: AuthScheme.Basic({ username: 'user@example.com', password: 'existing-password', }), } ); // Basic auth connections are immediately active console.log('Connected:', connection.id); ``` ## Updating credentials [#updating-credentials] When credentials expire or rotate, update them in place without recreating the connection. Fields you omit are preserved. Fields set to `null` are removed. **Bearer token:** ```python composio.connected_accounts.update( "ca_your_connection_id", connection={ "state": { "authScheme": "BEARER_TOKEN", "val": {"token": "new-access-token"}, }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); await composio.connectedAccounts.update('ca_your_connection_id', { connection: { state: { authScheme: 'BEARER_TOKEN', val: { token: 'new-access-token' }, }, }, }); ``` **curl:** ```bash curl -X PATCH https://backend.composio.dev/api/v3.1/connected_accounts/ca_xxx \ -H 'x-api-key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"connection":{"state":{"authScheme":"BEARER_TOKEN","val":{"token":"new-access-token"}}}}' ``` **API key:** ```python composio.connected_accounts.update( "ca_your_connection_id", connection={ "state": { "authScheme": "API_KEY", "val": {"generic_api_key": "new-api-key"}, }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); await composio.connectedAccounts.update('ca_your_connection_id', { connection: { state: { authScheme: 'API_KEY', val: { generic_api_key: 'new-api-key' }, }, }, }); ``` **curl:** ```bash curl -X PATCH https://backend.composio.dev/api/v3.1/connected_accounts/ca_xxx \ -H 'x-api-key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"connection":{"state":{"authScheme":"API_KEY","val":{"generic_api_key":"new-api-key"}}}}' ``` **Basic auth:** ```python composio.connected_accounts.update( "ca_your_connection_id", connection={ "state": { "authScheme": "BASIC", "val": {"username": "user@example.com", "password": "new-password"}, }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); await composio.connectedAccounts.update('ca_your_connection_id', { connection: { state: { authScheme: 'BASIC', val: { username: 'user@example.com', password: 'new-password' }, }, }, }); ``` **curl:** ```bash curl -X PATCH https://backend.composio.dev/api/v3.1/connected_accounts/ca_xxx \ -H 'x-api-key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"connection":{"state":{"authScheme":"BASIC","val":{"username":"user@example.com","password":"new-password"}}}}' ``` ## Using in your session [#using-in-your-session] Pass the auth config or connection ID when creating a session: **Python:** ```python session = composio.create( "user_123", auth_configs={"gmail": "ac_your_auth_config"}, toolkits=["gmail"], ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); const session = await composio.create('user_123', { authConfigs: { gmail: 'ac_your_auth_config' }, toolkits: ['gmail'], }); ``` ## Next [#next] - [Managing multiple accounts](/docs/authentication/managing-multiple-connected-accounts): Pin and select connected accounts for a user --- # Managed vs custom auth (/docs/authentication/custom-app-vs-managed-app) Composio supports two ways to authenticate users with toolkits. * **[Composio managed apps](/toolkits/managed-auth)**: Composio registers and maintains OAuth apps for popular toolkits such as GitHub, Gmail, and Slack. You do not need to register your own OAuth app. * **Custom auth configs**: You provide your own OAuth app, API key, bearer token, or other credentials and tell Composio to use them for a toolkit. This page covers when to use each, how to create a custom auth config, and how to wire it into a session. ## When to use Composio managed apps [#when-to-use-composio-managed-apps] Managed apps are the default when they are available. Use them when: * **You're building and iterating.** No OAuth app registration, no credentials to manage. Create a session and start testing immediately. * **Default scopes cover your needs.** Composio requests sensible defaults for each toolkit. * **Branding on consent screens doesn't matter yet.** Users will see "Composio wants to access your account" during OAuth. Fine for internal tools, prototypes, and development. You can still [white-label the Connect Link page](/docs/authentication/white-labeling-authentication#customizing-the-connect-link) with your logo and app title without needing your own OAuth app. ## When to use a custom auth config [#when-to-use-a-custom-auth-config] Bring your own credentials when any of these apply: * **Your users see OAuth consent screens.** In production, users should see your app name, not "Composio." This is the most common reason to switch. * **You need custom scopes.** Composio's default scopes may not include everything you need (e.g., write access to a specific Google API). * **You're hitting rate limits.** Managed apps share quota across all Composio users. Your own app gets a dedicated quota. * **You need faster polling triggers.** Managed auth enforces a 15-minute minimum polling interval; your own app can use shorter polling intervals where supported. * **You're connecting to a custom instance.** Self-hosted or regional variants (e.g., a private Salesforce subdomain) need their own OAuth app. * **Enterprise customers require your branding end-to-end.** ## Create a custom auth config [#create-a-custom-auth-config] To check whether Composio provides the OAuth app for a toolkit, see [Managed OAuth apps](/toolkits/managed-auth). You can still create a custom auth config for branding, scopes, rate limits, polling intervals, or custom instances. The steps below use the dashboard. To create auth configs in code instead, for example one per customer, see [Programmatic auth configs](/docs/authentication/programmatic-auth-configs). #### Create the auth config in the Composio dashboard In the [Composio dashboard](https://dashboard.composio.dev/~/project/auth-configs?utm_source=docs\&utm_medium=content\&utm_campaign=docs-custom-app-vs-managed-app): 1. Click **Create Auth Config** 2. Select the toolkit 3. Choose the auth scheme (OAuth2, API Key, Bearer Token, etc.) 4. Follow the dashboard instructions for the required credential fields For OAuth toolkits, the dashboard shows the redirect URI to add in the provider's developer portal. #### Collect credentials from the provider For OAuth toolkits, register an app in the provider's developer portal and add the redirect URI from the dashboard. Then copy the **Client ID** and **Client Secret** back into Composio. For API key, bearer token, basic auth, or other auth schemes, collect the credential fields the toolkit requires and enter them in the dashboard. Step-by-step OAuth guides: [Google](https://composio.dev/auth/googleapps) | [GitHub](https://composio.dev/auth/github) | [Slack](https://composio.dev/auth/slack) | [HubSpot](https://composio.dev/auth/hubspot) | [All toolkits](https://composio.dev/auth) #### Save and copy the auth config ID After you enter the required credentials, click **Create** and copy the auth config ID (for example, `ac_1234abcd`). #### Pass the auth config ID in your session Creating the auth config isn't enough on its own. A session uses your config only when you pass its ID to `authConfigs`, keyed by toolkit. Toolkits you leave out keep using Composio managed auth. **Python:** ```python from composio import Composio composio = Composio() session = composio.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", # toolkits not listed here still use Composio managed auth }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create("user_123", { authConfigs: { github: "ac_your_github_config", // toolkits not listed here still use Composio managed auth }, }); ``` ## Mixing per toolkit [#mixing-per-toolkit] You don't have to pick one approach for all toolkits. Use your own credentials for toolkits where users see consent screens (GitHub, Google, Slack) and Composio managed auth for the rest. Each toolkit gets its own auth config independently. **Python:** ```python session = composio.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", "google": "ac_your_google_config", # everything else uses Composio managed auth automatically }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create("user_123", { authConfigs: { github: "ac_your_github_config", google: "ac_your_google_config", // everything else uses Composio managed auth automatically }, }); ``` ### Toolkits without managed auth [#toolkits-without-managed-auth] Some OAuth toolkits do not have a Composio-managed OAuth app. Check [Managed OAuth apps](/toolkits/managed-auth) to find them. For API keys, instance details, and other authentication methods, check the individual pages in the [toolkit catalog](/toolkits). ## Next [#next] - [Programmatic auth configs](/docs/authentication/programmatic-auth-configs): Create auth configs in code and pass them to a session --- # Programmatic auth configs (/docs/authentication/programmatic-auth-configs) An [auth config](/docs/authentication#behind-the-scenes) is a blueprint for how a toolkit authenticates: the method, scopes, and credentials. Most of the time you create one in the [dashboard](https://dashboard.composio.dev/~/project/auth-configs?utm_source=docs\&utm_medium=content\&utm_campaign=docs-programmatic-auth-configs) and reuse it. Create them in code when you provision auth dynamically: a config per customer, per environment, or spun up and torn down as part of your app's lifecycle. `composio.authConfigs.create()` returns an auth config ID like `ac_xxxxxxxx`. Store that ID, then [pass it to a session](#use-the-auth-config-in-a-session) so the session authenticates with it. ## Composio managed auth [#composio-managed-auth] For OAuth2 toolkits, Composio maintains a managed app so you can create an auth config without bringing your own credentials. This is the fastest way to start. **Python:** ```python from composio import Composio composio = Composio() auth_config = composio.auth_configs.create( toolkit="github", options={"type": "use_composio_managed_auth", "name": "GitHub"}, ) print(auth_config.id) # ac_xxxxxxxx ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('github', { type: 'use_composio_managed_auth', name: 'GitHub', }); console.log(authConfig.id); // ac_xxxxxxxx ``` ## Your own OAuth2 credentials [#your-own-oauth2-credentials] Bring your own OAuth app to show your branding on consent screens, request custom scopes, or get a dedicated rate-limit quota. Register the app in the provider's developer portal, set its authorized redirect URI to Composio's callback, then pass the client ID and secret. ``` https://backend.composio.dev/api/v1/auth-apps/add ``` **Python:** ```python import os from composio import Composio composio = Composio() auth_config = composio.auth_configs.create( toolkit="notion", options={ "type": "use_custom_auth", "auth_scheme": "OAUTH2", "name": "Notion", "credentials": { "client_id": os.environ["NOTION_CLIENT_ID"], "client_secret": os.environ["NOTION_CLIENT_SECRET"], "oauth_redirect_uri": "https://backend.composio.dev/api/v1/auth-apps/add", }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('notion', { type: 'use_custom_auth', authScheme: 'OAUTH2', name: 'Notion', credentials: { client_id: process.env.NOTION_CLIENT_ID!, client_secret: process.env.NOTION_CLIENT_SECRET!, oauth_redirect_uri: 'https://backend.composio.dev/api/v1/auth-apps/add', }, }); ``` > Omit `oauth_redirect_uri` to use Composio's default callback. Set it only when you [route the callback through your own domain](/docs/authentication/white-labeling-authentication#routing-the-callback-through-your-domain). ## Other auth types [#other-auth-types] Toolkits that use API keys, bearer tokens, basic auth, or no auth follow the same call. Set `auth_scheme` to the toolkit's scheme and put the required fields in `credentials`. For a toolkit whose key the user supplies at connect time, pass empty `credentials`. **Python:** ```python auth_config = composio.auth_configs.create( toolkit="perplexityai", options={ "type": "use_custom_auth", "auth_scheme": "API_KEY", "name": "Perplexity AI", "credentials": {}, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('perplexityai', { type: 'use_custom_auth', authScheme: 'API_KEY', name: 'Perplexity AI', credentials: {}, }); ``` ## Use the auth config in a session [#use-the-auth-config-in-a-session] Creating an auth config does not change which credentials a session uses. Pass the auth config ID to `authConfigs` (keyed by toolkit) when you create the session, and the session authenticates that toolkit with your config. Toolkits you leave out keep using Composio managed auth. **Python:** ```python session = composio.sessions.create( user_id="user_123", auth_configs={"notion": auth_config.id}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const authConfig = { id: 'ac_your_notion_config' }; const session = await composio.create('user_123', { authConfigs: { notion: authConfig.id }, }); ``` See [Configuring sessions](/docs/configuring-sessions#custom-auth-configs) for how `authConfigs` interacts with account selection and precedence. ## Find auth configs [#find-auth-configs] In TypeScript, filter `authConfigs.list()` by name or ID with `search`. Disabled configs are excluded by default; set `showDisabled` to include them. Auth config responses use `id` as their canonical identifier. ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const configs = await composio.authConfigs.list({ search: 'github', showDisabled: true, }); for (const config of configs.items) { console.log(config.id); } ``` ## Discover the required fields [#discover-the-required-fields] Different schemes need different credential fields. To build the `credentials` object dynamically, ask the toolkit which fields it requires for a given scheme before you create the config. **Python:** ```python fields = composio.toolkits.get_auth_config_creation_fields( toolkit="notion", auth_scheme="OAUTH2", required_only=True, ) print(fields) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const fields = await composio.toolkits.getAuthConfigCreationFields('notion', 'OAUTH2', { requiredOnly: true, }); console.log(fields); ``` ## Next [#next] - [Controlling scopes](/docs/authentication/controlling-scopes): Override the default OAuth scopes Composio requests for a toolkit --- # Controlling scopes (/docs/authentication/controlling-scopes) Scopes are the permissions an OAuth toolkit grants your app: read email, write to a repo, manage calendar events. Composio requests a sensible set of default scopes for each toolkit, so most apps never set scopes at all. Override them when the defaults grant too much or too little: to follow least privilege, or to reach an API the defaults don't cover. You control scopes on an [auth config](/docs/authentication#behind-the-scenes), then [pass that auth config to a session](#use-the-auth-config-in-a-session) so the session requests your scopes when users connect. > Scopes apply to OAuth toolkits. Toolkits that authenticate with API keys or bearer tokens don't have scopes to set. ## Set scopes with Composio managed auth [#set-scopes-with-composio-managed-auth] Pass a `scopes` field in `credentials` to override the defaults while still using Composio's managed OAuth app. Give scopes as a comma-separated string. **Python:** ```python from composio import Composio composio = Composio() auth_config = composio.auth_configs.create( toolkit="hubspot", options={ "type": "use_composio_managed_auth", "name": "HubSpot", "credentials": {"scopes": "sales-email-read,tickets"}, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('hubspot', { type: 'use_composio_managed_auth', name: 'HubSpot', credentials: { scopes: 'sales-email-read,tickets' }, }); ``` ## Set scopes with your own OAuth app [#set-scopes-with-your-own-oauth-app] When you bring your own OAuth credentials, put `scopes` alongside the client ID and secret. Make sure your OAuth app has those scopes approved in the provider's portal. **Python:** ```python import os auth_config = composio.auth_configs.create( toolkit="github", options={ "type": "use_custom_auth", "auth_scheme": "OAUTH2", "name": "GitHub", "credentials": { "client_id": os.environ["GITHUB_CLIENT_ID"], "client_secret": os.environ["GITHUB_CLIENT_SECRET"], "scopes": "repo,read:org", }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('github', { type: 'use_custom_auth', authScheme: 'OAUTH2', name: 'GitHub', credentials: { client_id: process.env.GITHUB_CLIENT_ID!, client_secret: process.env.GITHUB_CLIENT_SECRET!, scopes: 'repo,read:org', }, }); ``` ## Update scopes on an existing config [#update-scopes-on-an-existing-config] Change the scopes on an auth config you already created without recreating it. **Python:** ```python composio.auth_configs.update( "ac_1234", {"type": "default", "scopes": "repo,read:org,read:user"}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); await composio.authConfigs.update('ac_1234', { type: 'default', scopes: 'repo,read:org,read:user', }); ``` > Changing scopes affects new connections only. Users with an existing [connected account](/docs/authentication#behind-the-scenes) keep the scopes they already granted until they reconnect. To apply new scopes to a current user, have them re-authenticate. ## Use the auth config in a session [#use-the-auth-config-in-a-session] Setting scopes on an auth config does nothing until a session uses it. Pass the auth config ID to `authConfigs` (keyed by toolkit) when you create the session, and the session requests your scopes when the user connects that toolkit. **Python:** ```python session = composio.create( user_id="user_123", auth_configs={"github": auth_config.id}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const authConfig = { id: 'ac_your_github_config' }; const session = await composio.create('user_123', { authConfigs: { github: authConfig.id }, }); ``` ## Next [#next] - [White-labeling authentication](/docs/authentication/white-labeling-authentication): Remove Composio branding from your auth flows --- # White-labeling authentication (/docs/authentication/white-labeling-authentication) There are four places where Composio branding shows up during authentication: | Where | What users see | How to fix | | --------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------- | | [**Connect Link page**](#customizing-the-connect-link) | Composio logo, name, and styling on the hosted auth page | Set your logo, app title, and theme in the dashboard | | [**OAuth consent screen**](#using-your-own-oauth-apps) | "Composio wants to access your account" on Google, GitHub, etc. | Use your own OAuth app | | [**Browser address bar**](#routing-the-callback-through-your-domain) | `backend.composio.dev` flashes during OAuth redirect-back | Proxy the redirect through your domain | | [**Post-auth success page**](#redirecting-users-after-authentication) | Composio-branded success page after OAuth completes | Pass a `callbackUrl` when initiating the connection | ## Customizing the Connect Link [#customizing-the-connect-link] The Connect Link is the hosted page your users see when connecting their accounts. By default it shows Composio branding on a neutral light theme. You can swap in your logo and name, then restyle the whole page to match your product. Everything here lives in **Project Settings** → [**White Labeling**](https://dashboard.composio.dev/~/project/settings/auth-screen?utm_source=docs\&utm_medium=content\&utm_campaign=docs-white-labeling-authentication). Changes apply to every Connect Link flow across all toolkits, for both [in-chat](/docs/authentication#in-chat-authentication) and [manual](/docs/authentication/manually-authenticating) authentication. Each project has one branding and one theme, so if you need a different look per product, use separate projects. ### Logo and name [#logo-and-name] 1. Go to **Project Settings** → **White Labeling**, and open the **Branding** tab. 2. Set your **App Title** and upload your **Logo** (a square JPEG or PNG, 256×256 to 1024×1024 pixels). The logo replaces the Composio mark at the top of the page, and the app title replaces "Composio" in the "...wants to connect to your account" heading. > This only changes the Composio-hosted page. For OAuth toolkits like Gmail, Google Sheets, GitHub, and Slack, users still see a consent screen saying "Composio wants to access your account." To change that, and to remove the "Secured by Composio" badge, set up your own OAuth app as described [below](#using-your-own-oauth-apps). ### Colors, fonts, and per-element styling [#colors-fonts-and-per-element-styling] Open the **Styling** tab to restyle the entire Connect Link page. A live preview sits beside the controls and updates as you edit, and you can preview each state of the flow with the **Welcome**, **Form**, **Success**, and **Error** tabs. ![The White Labeling editor with a live preview on the left and styling controls on the right](/images/auth-screen-theme-editor.png) *Editing colors, fonts, and per-element styling with a live preview of the Connect Link* Set the page-wide defaults first: * **Seed colours** for the page background, card, foreground text, and primary accent, plus secondary and tertiary accents for decorative touches. * **Typefaces**: a display font for headings and a body font, each chosen from a curated set. The default is ABC Diatype. * **Geometry**: corner radius and border width, which set the roundness and outline weight of cards, buttons, and inputs. For finer control, select any element in the preview to style just that element. Editable elements cover the page and card surfaces, the heading, body, and field labels, the primary and secondary buttons, the input, the error notice, links, and the logo. Depending on the element you can set its colour, background, border, corner radius, shadow, and text size, weight, letter spacing, and case. > **Contrast is enforced**: Text has to stay legible against its background. The editor shows the contrast ratio for each text element and disables **Save Changes** until every element passes, so you can't ship an unreadable page by accident. Prefer to work in code? Flip the **JSON** toggle to edit the theme as a single object you can paste, review, or generate. Your logo and app title stay in the **Branding** tab, since the logo has no JSON representation. > **Troubleshooting**: * **"Secured by Composio" badge won't go away:** this badge is removed when you use your own OAuth app. See [Using your own OAuth apps](#using-your-own-oauth-apps). * **Logo doesn't appear after uploading:** clear your browser cache or try incognito. * **Upload fails with "failed to fetch":** retry or use a smaller image. * **You see the Branding tab but no Styling tab:** theming is still rolling out. If it isn't enabled for your project yet, reach out. The logo and app title keep working in the meantime. ## Using your own OAuth apps [#using-your-own-oauth-apps] OAuth toolkits like Google and GitHub show a consent screen that says which app is requesting access. By default this reads "Composio wants to connect to your account." To show your app name instead, create a custom auth config with your own OAuth credentials and pass that auth config when creating a session. > **You don't need this for every toolkit**: Only white-label toolkits where users see a consent screen (Google, GitHub, Slack, etc.). Toolkits that use API keys don't show consent screens, so there's nothing to white-label. You can mix and match freely. - [Managed vs custom auth](/docs/authentication/custom-app-vs-managed-app): Decide when to use custom credentials, create an auth config, and pass it to sessions. ### Switching from Composio-managed to your own OAuth app [#switching-from-composio-managed-to-your-own-oauth-app] Existing connected accounts are tied to the auth config they were created with. Switching to a custom auth config affects new connections for that toolkit; existing users keep using their current connected accounts until they re-authenticate or you import/migrate their credentials. * To use the custom config for new connections, pass `authConfigs` when creating or updating the session. * Existing connections continue refreshing with their original auth config. * To fully migrate an existing user, delete the old connected account and have them re-authenticate with the new auth config, or import their credentials into the new config where supported. ## Routing the callback through your domain [#routing-the-callback-through-your-domain] During OAuth, the browser briefly redirects through `backend.composio.dev` so Composio can capture the auth token. Some toolkits also display this URL on the consent screen. If you need to hide Composio's domain, you can proxy the redirect through your own domain instead. #### Set the redirect URI to your domain In your OAuth app's settings, set the authorized redirect URI to your own endpoint: ``` https://yourdomain.com/api/composio-redirect ``` #### Create a proxy endpoint This endpoint receives the OAuth callback and immediately 302-redirects it to Composio: **Python:** ```python from fastapi import FastAPI, Request from fastapi.responses import RedirectResponse app = FastAPI() @app.get("/api/composio-redirect") def composio_redirect(request: Request): composio_url = "https://backend.composio.dev/api/v1/auth-apps/add" return RedirectResponse(url=f"{composio_url}?{request.url.query}") ``` **TypeScript:** ```typescript // pages/api/composio-redirect.ts (Next.js) import type { NextApiRequest, NextApiResponse } from "next"; export default function handler(req: NextApiRequest, res: NextApiResponse) { const composioUrl = "https://backend.composio.dev/api/v1/auth-apps/add"; const params = new URLSearchParams(req.query as Record); res.redirect(302, `${composioUrl}?${params.toString()}`); } ``` > Your endpoint must return a **302 redirect**. Do not follow the redirect server-side or make a fetch call to Composio. The user's browser needs to be redirected so the OAuth flow completes correctly. #### Update your auth config In the Composio dashboard, update your auth config to use your custom redirect URI. ![Auth Config Settings](/images/custom-redirect-uri.png) *Setting the custom redirect URI in your auth config* Here's how the redirect flow works. Your proxy just forwards the browser redirect to Composio. It never touches the authorization code or token. > For FAQs and setup guides for individual toolkits, browse the [toolkits page](/toolkits). ## Redirecting users after authentication [#redirecting-users-after-authentication] By default, after OAuth completes, users land on a Composio-hosted success page that shows Composio branding. To bypass this page and send users to your own domain instead, pass a `callbackUrl` when calling `session.authorize()`: **Python:** ```python connection_request = session.authorize( "gmail", callback_url="https://your-app.com/callback" ) ``` **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", }); ``` After authentication, Composio redirects the user to your callback URL instead of the default success page. For full details on the parameters appended to your callback URL, see [Manually authenticating users → Redirecting users after authentication](/docs/authentication/manually-authenticating#redirecting-users-after-authentication). ## Next [#next] - [Managed vs custom auth](/docs/authentication/custom-app-vs-managed-app): Set up auth configs for OAuth apps, API keys, and toolkits without managed auth --- # Triggers (/docs/triggers) When something happens in a connected app (a new Slack message, a GitHub commit, an incoming email), a **trigger** sends that event to your app as a structured payload. You write the handler. Composio handles the connection to the provider, delivery, retries, and signing. ## Where events arrive [#where-events-arrive] Composio delivers every event to one destination you control: your webhook URL. You register it once per project, and Composio `POST`s every trigger event there, signed so you can verify it. ## Realtime vs polling [#realtime-vs-polling] Under the hood, Composio learns about events in one of two ways. You don't configure this. It's a property of the trigger type, and it only affects how quickly an event reaches you. | Kind | How Composio learns about the event | Latency | Examples | | ------------ | ---------------------------------------------------------------- | --------------------------------------- | ----------------------------- | | **Realtime** | The provider pushes the event to Composio the moment it happens. | Near-instant | Slack, Asana, Notion, Outlook | | **Polling** | Composio checks the provider on a schedule. | Up to \~15 min on Composio-managed auth | Gmail, Google Calendar | Either way, the event lands in the same place: your subscription or webhook URL, in the same payload shape. > If you **bring your own OAuth app**, some providers only deliver to URLs registered on that app, so you register Composio's ingress URL there once. See [Custom OAuth webhooks](/docs/setting-up-triggers/custom-oauth-webhooks). ## Trigger types and instances [#trigger-types-and-instances] A **trigger type** is a kind of event you can listen for, like `GITHUB_COMMIT_EVENT` or a new Slack message. Each toolkit has its own set. A **trigger instance** is a trigger type you've activated for one [user's connected account](/docs/how-composio-works). It has its own `ti_*` ID that you can enable, disable, or delete independently. ## Working with triggers [#working-with-triggers] 0. **Authenticate** the user for the toolkit: an [auth config](/docs/authentication#behind-the-scenes) and a [connected account](/docs/authentication). See [Authentication](/docs/authentication). 1. **Create** a trigger for the user's connected account. See [Creating triggers](/docs/setting-up-triggers/creating-triggers). 2. **Receive** its events: locally with `subscribe()`, or in production at your webhook URL. See [Receiving events](/docs/setting-up-triggers/subscribing-to-events). 3. **Manage** triggers: enable, disable, or delete. See [Managing triggers](/docs/setting-up-triggers/managing-triggers). ## Next [#next] - [Creating triggers](/docs/setting-up-triggers/creating-triggers): Activate a trigger for a user so events start flowing --- # Skills (/docs/skills) A **skill** is an execution playbook for one concrete task: "send an email to someone", "post a message to a Slack channel", "query a Notion database". It records the tools the task needs, the order to call them in, and the mistakes that make it fail. You don't install skills or reference them by ID. When your agent calls [`COMPOSIO_SEARCH_TOOLS`](/toolkits/meta-tools/search_tools) to find the tools for a task, a skill covering that task comes back in the same response: ```json { "primary_tool_slugs": ["SLACK_FIND_CHANNELS", "SLACK_SEND_MESSAGE"], "related_tool_slugs": ["SLACK_FIND_USERS"], "difficulty": "easy - Simple single-tool operation with known parameters", "recommended_plan_steps": [ "Resolve the channel ID with SLACK_FIND_CHANNELS before posting.", "Send the message with SLACK_SEND_MESSAGE using the resolved ID." ], "known_pitfalls": [ "Passing a channel name where the API expects an ID returns channel_not_found." ] } ``` What just happened: your agent asked for tools and got a sequence. `primary_tool_slugs` and `related_tool_slugs` come back on every search. `recommended_plan_steps`, `known_pitfalls`, and `difficulty` appear only when a skill covers the use case, so treat them as optional in your handling. Composio derives skills from real usage across the platform rather than writing them by hand, so they reflect how these tasks get completed, not how they ought to work. ## Why skills matter [#why-skills-matter] Handing an agent a toolkit tells it *what* it can call. It doesn't tell it *how* the call usually goes wrong, and an agent that works this out on its own pays for it in tokens and retries: * It calls a tool with the wrong identifier, reads the error, and tries again. * It fetches a whole inbox when it needed one search. * It skips a lookup step and posts to a channel that never resolves. A skill front-loads that knowledge. The sequence is already known and so are the pitfalls, so the work of rediscovering them is never spent. Because the skill arrives inside the search response, it lands in your agent's context before the first execution: the sequence is right the first time, and the known failure modes are avoided rather than discovered from an error. > Skills are read-only. Search matches them to the use case your agent describes, so there is nothing to install, enable, or configure. They are part of search results by default, and there is no opt-out today. ## What a skill contains [#what-a-skill-contains] **Use case.** The task in plain language, for example *Start a direct message with someone in Slack and send a message*. This is what search matches against. **Tools.** The tool slugs the task uses, such as `SLACK_FIND_USERS`, `SLACK_OPEN_DM`, and `SLACK_SEND_MESSAGE`. A skill distinguishes the main tools from the supporting ones. **Execution plan.** The ordered steps for completing the task, including optional steps and fallback paths for when the primary route is unavailable. **Pitfalls.** The known failure modes for this task: the wrong-identifier mistakes, the missing lookups, and the assumptions that don't hold. A skill isn't limited to one app. A task may span Slack and Gmail, and a skill covering several toolkits appears under each of them. ## How a skill reaches your agent [#how-a-skill-reaches-your-agent] Skills arrive through the search step a [session](/docs/how-composio-works) already performs: 1. You create a session and give your agent its meta tools. 2. Your agent has something to do, so it calls `COMPOSIO_SEARCH_TOOLS` with the task in plain language, one `use_case` per query. 3. Composio searches for tools and for a skill covering that use case at the same time. 4. The response carries the tool slugs and their schemas. When a skill covers the use case, that same response carries the plan and pitfalls. 5. Your agent works through the returned steps in order and checks the pitfalls as it goes, instead of inferring a sequence from the tool schemas. Search matches on the use case, not on a tool name, so the phrasing your agent uses matters. "Start a DM with someone in Slack and send them a message" matches a skill. "slack tools" does not. Note that skills ride inside the search response, so there is no separate contract to code against and nothing in your application has to ask for them. > There is no API for listing or reading skills, so unlike most of Composio there is no cURL equivalent on this page. Your agent still receives them at runtime through search. This page will be updated when that changes. ## Related [#related] * [How Composio works](/docs/how-composio-works): sessions, meta tools, and where search fits. * [`COMPOSIO_SEARCH_TOOLS`](/toolkits/meta-tools/search_tools): the search meta tool and its response fields. * [Configuring sessions](/docs/configuring-sessions): how you give an agent access to tools. --- # Using sessions via MCP (/docs/sessions-via-mcp) Use this guide when you are building an application with Composio and want to expose one user's session over MCP. The application creates and configures the session, then passes its hosted MCP endpoint to a compatible client. By default, Composio gives your agent tools it can call directly through a [provider package](/docs/providers). That is what the [Quickstart](/docs/quickstart) uses. Set `mcp: true` when an MCP transport fits your application better. No provider package is required for this route. > **Connecting an existing agent instead?**: If you use Codex or Claude Code and did not explicitly choose MCP, install the native [Composio agent plugin](/docs/agent-plugins). If you explicitly want MCP in an existing client, use [Composio Connect](/docs/composio-connect). > Want to bring tools from your own remote MCP server into Composio instead? See [Custom MCP](/docs/extending-sessions/custom-mcp). ## The MCP endpoint [#the-mcp-endpoint] Opt into MCP by passing `mcp: true` when you create the session. The session then exposes its hosted MCP server. Read the URL and headers off `session.mcp`: **Python:** ```python from composio import Composio composio = Composio() session = composio.sessions.create(user_id="user_123", mcp=True) mcp_url = session.mcp.url mcp_headers = session.mcp.headers ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio(); const session = await composio.create("user_123", { mcp: true }); const mcpUrl = session.mcp.url; const mcpHeaders = session.mcp.headers; ``` You don't need a provider package to use the MCP endpoint, so you can drop it from your `Composio()` setup if MCP is all you need. > Resuming a stored session? Pass the same flag, `composio.use(sessionId, { mcp: true })` (TypeScript) or `composio.use(session_id, mcp=True)` (Python), to surface `session.mcp` on the reused session. > The MCP endpoint and `session.tools()` are backed by the same session. Toolkits, auth configs, and connected accounts you set when [configuring the session](/docs/configuring-sessions) apply to both. ## A single URL for a fixed set of tools [#a-single-url-for-a-fixed-set-of-tools] Combine `mcp: true` with the [direct-tools preset](/docs/configuring-sessions) to get one MCP URL that serves exactly the tools you list, with no search or meta tools in front of them. This is the closest equivalent to a classic hosted MCP server scoped to a handful of tools. **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS composio = Composio() session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], tools={"gmail": {"enable": ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"]}}, session_preset=SESSION_PRESET_DIRECT_TOOLS, mcp=True, ) # A single MCP URL that exposes just these two tools print(session.mcp.url) ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from "@composio/core"; const composio = new Composio(); const session = await composio.create("user_123", { toolkits: ["gmail"], tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"] } }, sessionPreset: SessionPreset.DIRECT_TOOLS, mcp: true, }); // A single MCP URL that exposes just these two tools console.log(session.mcp.url); ``` Any MCP client pointed at that URL sees only `GMAIL_FETCH_EMAILS` and `GMAIL_CREATE_EMAIL_DRAFT`. See [Configuring Sessions](/docs/configuring-sessions) for the full set of toolkit, tool, and auth filters. ## Wire it into your framework [#wire-it-into-your-framework] Pass `session.mcp.url` and `session.mcp.headers` to your framework's MCP client. **OpenAI Agents (Python):** ```python from agents import Agent, HostedMCPTool agent = Agent( name="Assistant", tools=[ HostedMCPTool( tool_config={ "type": "mcp", "server_label": "composio", "server_url": session.mcp.url, "headers": session.mcp.headers, "require_approval": "never", } ) ], ) ``` **Claude Agent SDK (Python):** ```python from claude_agent_sdk import ClaudeAgentOptions options = ClaudeAgentOptions( mcp_servers={ "composio": { "type": "http", "url": session.mcp.url, "headers": session.mcp.headers, } }, ) ``` **Vercel AI SDK (TypeScript):** ```typescript import { Composio } from "@composio/core"; import { createMCPClient } from "@ai-sdk/mcp"; const composio = new Composio(); const { mcp } = await composio.create("user_123", { mcp: true }); const client = await createMCPClient({ transport: { type: "http", url: mcp.url, headers: mcp.headers, }, }); const tools = await client.tools(); ``` ## Trade-offs [#trade-offs] MCP is the more portable option. Any MCP-compatible client connects with just a URL, and it's supported across more frameworks and apps (Claude Desktop, Cursor, the OpenAI Responses API, and others) without a provider package. The trade-off is that the MCP client talks to Composio's server directly, so anything the SDK does *around* tool execution doesn't apply: * **Tool-call modifiers don't run.** `beforeExecute` / `afterExecute` hooks and `modifySchema` transforms live in the SDK's execution path. Over MCP the client executes tools against the server and bypasses them, so you can't intercept, reshape, log, or gate calls the way you can with tools your agent calls directly. * **Session-bound custom tools and toolkits don't work.** Tools created with `experimental_createTool` / `experimental_createToolkit` in TypeScript, or `composio.experimental.tool` / `composio.experimental.Toolkit` in Python, run in your process. The MCP server only exposes Composio's hosted tools, so your local custom tools aren't available over the endpoint. If you need any of those, call tools directly through a [provider](/docs/providers) instead of over MCP. ## Next [#next] - [Configuring Sessions](/docs/configuring-sessions): Restrict toolkits, set custom auth configs, and select connected accounts --- # Remote sandbox (/docs/sandbox/remote) The **sandbox** is a persistent Python environment where your agent writes and executes code. It has programmatic access to all Composio tools, plus helper functions for calling LLMs, uploading files, and making API requests. State persists across calls within a [session](/docs/how-composio-works). Your agent runs code in it through the `COMPOSIO_REMOTE_WORKBENCH` meta tool, and shell commands through `COMPOSIO_REMOTE_BASH_TOOL`. > **Renamed from workbench**: This feature used to be called the **workbench**. The preferred session config key is now `sandbox`, but `workbench` still works as a fully supported alias, in both SDKs and on the wire. It isn't deprecated, so existing code keeps running unchanged. The `COMPOSIO_REMOTE_WORKBENCH` and `COMPOSIO_REMOTE_BASH_TOOL` meta tools keep their names. > **Composio doesn't run or replace your agent**: Your agent and its model stay entirely yours — Composio never proxies your LLM or runs an agent on your behalf. It discovers, authenticates, and executes tools. The one place Composio itself can call a model is *inside the sandbox*, and only when your code opts in — most directly through the [`invoke_llm`](#built-in-helpers) helper. That optional, sandbox-only usage is the sole source of any Composio-side **LLM tokens**. The sandbox is opt-in per [session](/docs/configuring-sessions) via `sandbox: { enable: true }` — don't enable it (or simply never call `invoke_llm`) and Composio uses no LLM tokens at all. ## Where it fits [#where-it-fits] Use the sandbox when a task is too complex for individual tool calls. Your agent starts with [`SEARCH_TOOLS` to find the right tools, then uses `MULTI_EXECUTE`](/docs/how-composio-works#meta-tools) for straightforward calls. When the task involves bulk operations, data transformations, or multi-step logic, the agent reaches for `COMPOSIO_REMOTE_WORKBENCH` instead. ## What the sandbox provides [#what-the-sandbox-provides] ### Built-in helpers [#built-in-helpers] These functions are pre-initialized in every sandbox, so your agent can call them without any setup: | Helper | What it does | | -------------------- | ----------------------------------------------------------------------------------------------------- | | `run_composio_tool` | Execute any Composio tool (e.g., `GMAIL_SEND_EMAIL`, `SLACK_SEND_MESSAGE`) and get structured results | | `invoke_llm` | Call an LLM for classification, summarization, content generation, or data extraction | | `upload_local_file` | Upload generated files (reports, CSVs, images) to cloud storage and get a download URL | | `proxy_execute` | Make direct API calls to connected services when no pre-built tool exists | | `web_search` | Search the web and return results for research or data enrichment | | `smart_file_extract` | Extract text from PDFs, images, and other file formats in the sandbox | ### Libraries [#libraries] The sandbox ships with common packages pre-installed: `pandas`, `numpy`, `matplotlib`, `Pillow`, `PyTorch`, and `reportlab`. Beyond these, the sandbox maintains a list of supported packages and their dependencies. If the agent uses a package that isn't already installed, the sandbox installs it automatically. ### Error correction [#error-correction] The sandbox corrects common mistakes in the code your agent generates. For example, if a script accesses `result["apiKey"]` but the actual field name is `api_key`, the sandbox resolves the mismatch instead of failing. ### Persistent state [#persistent-state] The sandbox runs as a persistent Jupyter notebook. Variables, imports, files, and in-memory state from one call are available in the next. ### Compute tier [#compute-tier] Sandboxes default to `standard` (1 vCPU, 1 GB RAM). For heavier workloads (large dataframes, ML preprocessing, or big bulk operations), pick a larger tier when creating the session via `sandbox.sandboxSize` (TypeScript) or `sandbox.sandbox_size` (Python). Available tiers: * `standard` (1 vCPU, 1 GB RAM) * `medium` (2 vCPU, 2 GB) * `large` (4 vCPU, 4 GB) * `xlarge` (8 vCPU, 8 GB) Larger tiers require `@composio/core` ≥ `0.8.1` or `composio` ≥ `0.12.1`. See [Configuring sessions → Sandbox compute tier](/docs/configuring-sessions#sandbox-compute-tier) for examples. > **Pricing:** Sandboxes are not billed today. Composio plans to begin billing for sandbox usage soon (metered by tier and runtime). ## Files and mounts [#files-and-mounts] The sandbox has a persistent file mount at `/mnt/files/`. Code running in the sandbox reads and writes files there, and the mount survives sandbox restarts: changing the [compute tier](#compute-tier) recreates the sandbox and clears in-memory state, but `/mnt/files/` persists. Move files between your app and the mount with `session.experimental.files`. Upload an input file and the agent reads it at `/mnt/files/`. When the agent writes a result, download it from your app. > The files API ships under `session.experimental`, so the surface may change in a future release. **Python:** ```python session = composio.create("user_123") # Upload a local file; the sandbox sees it at /mnt/files/sales.csv uploaded = session.experimental.files.upload("./sales.csv") print(uploaded.sandbox_mount_prefix, uploaded.mount_relative_path) # /mnt/files sales.csv # After the agent writes a result in the sandbox, download it report = session.experimental.files.download("/report.pdf") report.save("./report.pdf") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); // Upload a local file; the sandbox sees it at /mnt/files/sales.csv const uploaded = await session.experimental.files.upload("./sales.csv"); console.log(uploaded.sandboxMountPrefix, uploaded.mountRelativePath); // /mnt/files sales.csv // After the agent writes a result in the sandbox, download it const report = await session.experimental.files.download("/report.pdf"); await report.save("./report.pdf"); ``` The mount exposes four methods: | Method | What it does | | -------------------------- | --------------------------------------------------------------------------- | | `upload(input, options?)` | Upload from a local path, URL, `File`, or buffer. Returns a `RemoteFile`. | | `list(options?)` | List files under `path` on the mount, with `cursor` and `limit` pagination. | | `download(path, options?)` | Fetch a file from the mount as a `RemoteFile`. | | `delete(path, options?)` | Remove a file or directory from the mount. | A `RemoteFile` carries the file's bytes and a presigned `downloadUrl`. Read it with `text()` or `buffer()`, or write it to disk with `save(path)`. Its `expiresAt` is when that download link expires, not a TTL on the file: the mount itself has no expiry you set. Every file lives on the session's default `files` mount, surfaced at `/mnt/files/`. Each method takes a `mountId` to address a mount by ID, but there's no SDK call to create custom mounts today, so you'll normally work with `files`. ## Next [#next] - [Local sandbox](/docs/sandbox/local): Run the same tool calls in a sandbox you own, while Composio keeps managed auth and discovery. --- # Local sandbox (/docs/sandbox/local) A **local sandbox** runs your agent's code in your own infrastructure instead of Composio's hosted [remote sandbox](/docs/sandbox/remote), so code execution never leaves your security boundary. ## When to use it [#when-to-use-it] Reach for a local sandbox when: * **Sensitive code or data.** The agent runs untrusted code or touches data that can't leave your infrastructure. * **You already run sandboxes.** You'd rather run agent code in your own VM, container, or CI worker than a hosted runtime. * **You need your own filesystem and shell.** The task installs packages, runs a build, or shells out to local tools. If none of that applies, use the [remote sandbox](/docs/sandbox/remote). It's the same helper surface with no infrastructure to run. ## Create a local sandbox session [#create-a-local-sandbox-session] A local sandbox session is a [session](/docs/how-composio-works) created with the remote sandbox turned off. Set `workbench.enable: false`, then pass the session to `experimental_createLocalWorkbenchSession` (from the `@composio/experimental` package), which returns the two pieces you run yourself. ```typescript import { Composio } from '@composio/core'; import { experimental_createLocalWorkbenchSession } from '@composio/experimental/workbench'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); // Create the session with the remote sandbox disabled, so code runs in your box. const session = await composio.create('user_123', { toolkits: ['github'], workbench: { enable: false }, }); const { helperSource, env } = await experimental_createLocalWorkbenchSession(composio, session); ``` You get back two things: | Field | What it is | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `helperSource` | A Python helper, as source you write into your sandbox (for example as `composio_helper.py`). It exposes the in-sandbox tool surface the agent calls. | | `env` | The environment variables that helper needs to reach Composio from inside your box. Pass them to the process you run the agent in. | `experimental_createLocalWorkbenchSession` validates that the session is local: it throws if the session has the remote workbench enabled, because the remote sandbox and a local one can't both run for a single session. The session you pass in must have `workbench.enable: false`. > The API ships under the `experimental_` prefix and the `@composio/experimental/workbench` entry point, so the surface may change in a future release. ## The helper contract [#the-helper-contract] `helperSource` is a Python module your agent imports. It exposes the same helpers the [remote sandbox](/docs/sandbox/remote#built-in-helpers) runs, with the same signatures, so the agent calls tools without knowing that execution is local: ```python from composio_helper import run_composio_tool, invoke_llm, web_search response, error = run_composio_tool( "GITHUB_GET_A_PULL_REQUEST", {"owner": "composiohq", "repo": "composio", "pull_number": 1}, ) ``` Every `run_composio_tool` call routes back through the Tool Router under the session's connections, so auth and discovery stay managed. Most of the workbench helpers run in a local sandbox today. Only the file helpers, which depend on the managed `/mnt/files` mount, are remote-only: | Helper | What it does | Local sandbox | | -------------------- | ---------------------------------------------------------------------------------- | ------------- | | `run_composio_tool` | Execute any Composio tool | ✅ | | `invoke_llm` | Call an LLM | ✅ | | `web_search` | Search the web | ✅ | | `proxy_execute` | Call a toolkit API directly when no tool exists (needs a proxy-execute-scoped key) | ✅ | | `upload_local_file` | Upload generated files to storage | ❌ | | `smart_file_extract` | Extract text from PDFs and images | ❌ | If your agent needs one of the file helpers, use the [remote sandbox](/docs/sandbox/remote) for now. ## Security: the sandbox is your boundary [#security-the-sandbox-is-your-boundary] The helper reaches Composio with your project `COMPOSIO_API_KEY`, injected into the sandbox through `env`. Any code or output in the sandbox can read it, and that key acts across every connection on the project. > The sandbox is your security boundary. Isolate it like anything else holding a project credential, and rotate `COMPOSIO_API_KEY` if a run could have leaked it. ## Next [#next] - [What is a session?](/docs/how-composio-works): How sessions scope tools, auth, and sandbox state to a user --- # Proxy execute (/docs/extending-sessions/proxy-execute) `session.proxyExecute()` calls any HTTP endpoint on a toolkit your session can already reach, and Composio injects the authentication (OAuth token, API key, basic auth, and so on) on the server side. Your code never handles raw credentials. The session is already scoped to a [userID](/docs/how-composio-works), so you pass a `toolkit` slug rather than an account ID. Composio resolves the user's connected account for that toolkit and signs the request with it. Proxy execute is a building block, not just a fallback. Use it to build experiences on top of Composio that reach past predefined tools, like the [Pi + Slack bot example](/examples/general-agent-with-pi#reach-the-gaps-with-the-proxy), which drops down to the proxy for the Slack Web API calls the toolkit doesn't wrap as tools. We'll link more examples here as we add them. > **Use a scoped API key**: Proxy execute is gated behind its own permission. Authenticate with a [scoped project API key](/reference/authenticating-to-composio/project-api-key-permissions) that has the **Proxy execute** permission granted. Default full-access keys already include it. ## When to use it [#when-to-use-it] * **Endpoints with no predefined tool.** You need a specific endpoint (an unusual GitHub, LinkedIn, or Notion route) that isn't exposed as a Composio tool. Send it through the proxy instead of extracting the raw token and calling the API yourself. * **Request shapes a tool can't express.** Custom query parameters, partial field masks, or advanced filters on Gmail, Drive, Sheets, and similar. The proxy gives you the full HTTP surface of the upstream API while Composio keeps managing auth. ## Quick start [#quick-start] **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.create("user_123", toolkits=["github"]) response = session.proxy_execute( toolkit="github", endpoint="/repos/composiohq/composio/issues/1", method="GET", parameters=[ {"name": "Accept", "value": "application/vnd.github.v3+json", "in": "header"}, ], ) print(response["status"]) print(response["data"]) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create('user_123', { toolkits: ['github'] }); const { status, data } = await session.proxyExecute({ toolkit: 'github', endpoint: '/repos/composiohq/composio/issues/1', method: 'GET', parameters: [ { name: 'Accept', value: 'application/vnd.github.v3+json', in: 'header' }, ], }); console.log(status); console.log(data); ``` The `endpoint` is a path relative to the toolkit's base URL (`/repos/...` resolves against `api.github.com`). Pass an absolute URL only when you need a host that isn't the toolkit's standard API, such as a regional Salesforce or Zendesk domain. > Proxy execute rejects cross-domain requests. The `endpoint` must resolve to the same domain as the toolkit's connected account (a GitHub connection can only call `api.github.com` paths). This is an intentional security boundary, not a quota, so you can't work around it by reshaping the request. ## Parameters [#parameters] | Parameter | Required | Type | Description | | ------------ | -------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `toolkit` | Yes | `string` | Toolkit slug (`github`, `gmail`, and so on). Composio uses the session user's connected account for this toolkit. | | `endpoint` | Yes | `string` | Path relative to the toolkit's base URL, or an absolute URL. | | `method` | Yes | `"GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE"` | HTTP verb. | | `body` | No | `object` | JSON request body. Used with `POST`, `PUT`, and `PATCH`. | | `parameters` | No | `Array<{ name, value, in }>` | Extra headers or query parameters. `in` is `"header"` or `"query"`. | ## Response shape [#response-shape] The call forwards the upstream response's status, headers, and parsed body. Both SDKs expose the same fields; each spells them the way its language does, so TypeScript reads them as properties and Python as dictionary keys. | TypeScript | Python | Type | Description | | ------------ | ------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `status` | `status` | `number` / `int` | HTTP status code from the upstream API. | | `data` | `data` | `unknown` / `Any` | Parsed JSON body the API returned. Optional in TypeScript; in Python the key is always present but may be `None`. | | `headers` | `headers` | `Record` / `dict[str, str] \| None` | Response headers. Optional in TypeScript; in Python the key is always present but may be `None`. | | `binaryData` | `binary_data` | `object` / `dict` | Present only when the upstream returns a file. | When the upstream returns a file, the binary entry carries `url`, `contentType`, `size`, and `expiresAt` in TypeScript, and `url`, `content_type`, `size`, and `expires_at` in Python. `expires_at` may be absent in TypeScript and `None` in Python when the upstream gives no expiry. The key is absent entirely when the response is not a file, so check for it before reading: **Python:** ```python if "binary_data" in response: print(response["binary_data"]["url"]) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create('user_123', { toolkits: ['github'] }); const response = await session.proxyExecute({ toolkit: 'github', endpoint: '/repos/composiohq/composio/issues/1', method: 'GET', }); if (response.binaryData) { console.log(response.binaryData.url); } ``` > Don't set the `Authorization` header yourself through `parameters`. Composio injects the correct one from the connected account's auth scheme, and setting it manually overrides that credential and usually produces a `401`. ## Error handling [#error-handling] `status` and `data` reflect exactly what the toolkit API returned, so check `status` and branch on the common failures. | Status | Typical cause | How to resolve | | ----------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `400 Bad Request` | Malformed endpoint path, invalid body, or unsupported `method`. | Check the upstream API docs for the expected shape. The proxy doesn't validate upstream schemas. | | `401 Unauthorized` | The connected account's token expired or was revoked. | Re-authenticate the user, or [import fresh credentials](/docs/authentication/importing-existing-connections). | | `403 Forbidden` | The user's OAuth scopes or API key don't cover this endpoint. | Update the [auth config scopes](/docs/auth-configuration/custom-auth-configs) and have the user re-consent. | | `429 Too Many Requests` | Upstream rate limit (GitHub, Google, and so on). | Honor the `Retry-After` header and back off. Composio doesn't retry automatically. | ## Next [#next] - [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits): Define in-process tools and toolkits that run alongside Composio tools --- # Custom Tools and Toolkits (/docs/extending-sessions/custom-tools-and-toolkits) Custom tools let you define tools that run in-process alongside remote Composio tools within a session. You have three patterns: * **Standalone tools**: internal app logic that doesn't need Composio auth (DB lookups, in-memory data, business rules). * **Extension tools**: wrap a Composio toolkit's API with custom business logic via `extendsToolkit` / `extends_toolkit`, using `ctx.proxyExecute()` / `ctx.proxy_execute()` for authenticated requests. * **Custom toolkits**: group related standalone tools under a namespace. ### standalone **Install** **TypeScript:** **Python:** **Initialize the client** **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") ``` **Create the tool** A standalone tool handles internal app logic that doesn't need Composio auth. `ctx.userId` identifies which user's session is running. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; const profiles: Record = { "user_1": { name: "Alice Johnson", email: "alice@myapp.com", tier: "enterprise" }, "user_2": { name: "Bob Smith", email: "bob@myapp.com", tier: "free" }, }; const getUserProfile = experimental_createTool("GET_USER_PROFILE", { name: "Get user profile", description: "Retrieve the current user's profile from the internal directory", inputParams: z.object({}), execute: async (_input, ctx) => { const profile = profiles[ctx.userId]; if (!profile) throw new Error(`No profile found for user "${ctx.userId}"`); return profile; }, }); ``` **Python:** ```python from pydantic import BaseModel, Field from composio import Composio from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) class UserLookupInput(BaseModel): user_id: str = Field(description="User ID") USERS = { "user_1": {"name": "Alice Johnson", "email": "alice@myapp.com", "tier": "enterprise"}, "user_2": {"name": "Bob Smith", "email": "bob@myapp.com", "tier": "free"}, } @composio.experimental.tool() def get_user_profile(input: UserLookupInput, ctx): """Retrieve the current user's profile from the internal directory.""" profile = USERS.get(input.user_id) if not profile: raise ValueError(f'No profile found for user "{input.user_id}"') return profile ``` **Bind to a session** Pass custom tools via the `experimental` option. `session.tools()` returns both remote Composio tools and your custom tools. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; declare const getUserProfile: ReturnType; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1", { experimental: { customTools: [getUserProfile], }, }); const tools = await session.tools(); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.create( user_id="user_1", experimental={ "custom_tools": [get_user_profile], }, ) tools = session.tools() ``` ### extension **Install** **TypeScript:** **Python:** **Initialize the client** **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") ``` **Create the tool** An extension tool wraps a Composio toolkit's API with custom business logic. It inherits auth via `extendsToolkit` / `extends_toolkit`, so `ctx.proxyExecute()` / `ctx.proxy_execute()` handles credentials automatically. Prefer relative `endpoint` values in proxy calls. They resolve against the toolkit base URL. Composio only accepts absolute URLs when they stay on the same scheme and registrable domain as that base URL. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; const sendPromoEmail = experimental_createTool("SEND_PROMO_EMAIL", { name: "Send promo email", description: "Send the standard promotional email to a recipient", extendsToolkit: "gmail", inputParams: z.object({ to: z.string().describe("Recipient email address"), }), execute: async (input, ctx) => { const subject = "You're invited to try MyApp Pro"; const body = "Hi there,\n\nWe'd love for you to try MyApp Pro — free for 14 days.\n\nBest,\nThe MyApp Team"; const raw = btoa(`To: ${input.to}\r\nSubject: ${subject}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n${body}`); const res = await ctx.proxyExecute({ toolkit: "gmail", endpoint: "/gmail/v1/users/me/messages/send", method: "POST", body: { raw }, }); return { status: res.status, to: input.to }; }, }); ``` **Python:** ```python import base64 from pydantic import BaseModel, Field from composio import Composio composio = Composio(api_key="your_api_key") class PromoEmailInput(BaseModel): to: str = Field(description="Recipient email address") @composio.experimental.tool(extends_toolkit="gmail") def send_promo_email(input: PromoEmailInput, ctx): """Send the standard promotional email to a recipient.""" subject = "You're invited to try MyApp Pro" body = ( "Hi there,\n\n" "We'd love for you to try MyApp Pro — free for 14 days.\n\n" "Best,\nThe MyApp Team" ) raw_msg = ( f"To: {input.to}\r\n" f"Subject: {subject}\r\n" "Content-Type: text/plain; charset=UTF-8\r\n\r\n" f"{body}" ) raw = base64.urlsafe_b64encode(raw_msg.encode()).decode().rstrip("=") res = ctx.proxy_execute( toolkit="gmail", endpoint="/gmail/v1/users/me/messages/send", method="POST", body={"raw": raw}, ) return {"status": res["status"], "to": input.to} ``` **Bind to a session** Pass custom tools via the `experimental` option. Extension tools inherit auth from the toolkit specified in `extendsToolkit`. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; declare const sendPromoEmail: ReturnType; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1", { toolkits: ["gmail"], experimental: { customTools: [sendPromoEmail], }, }); const tools = await session.tools(); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.create( user_id="user_1", toolkits=["gmail"], experimental={ "custom_tools": [send_promo_email], }, ) tools = session.tools() ``` ### toolkit **Install** **TypeScript:** **Python:** **Initialize the client** **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") ``` **Create the toolkit** A custom toolkit groups related standalone tools under a namespace. Tools inside a toolkit cannot use `extendsToolkit`. **TypeScript:** ```typescript import { Composio, experimental_createTool, experimental_createToolkit, } from "@composio/core"; import { z } from "zod/v3"; const userManagement = experimental_createToolkit("USER_MANAGEMENT", { name: "User scoping", description: "Manage user roles and permissions", tools: [ experimental_createTool("ASSIGN_ROLE", { name: "Assign role", description: "Assign a role to a user in the internal system", inputParams: z.object({ user_id: z.string().describe("Target user ID"), role: z.enum(["admin", "editor", "viewer"]).describe("Role to assign"), }), execute: async ({ user_id, role }) => ({ user_id, role, assigned: true }), }), ], }); ``` **Python:** ```python from pydantic import BaseModel, Field from composio import Composio composio = Composio(api_key="your_api_key") user_management = composio.experimental.Toolkit( slug="USER_MANAGEMENT", name="User scoping", description="Manage user roles and permissions", ) class AssignRoleInput(BaseModel): user_id: str = Field(description="Target user ID") role: str = Field(description="Role to assign") @user_management.tool() def assign_role(input: AssignRoleInput, ctx): """Assign a role to a user in the internal system.""" return {"user_id": input.user_id, "role": input.role, "assigned": True} ``` **Bind to a session** Pass custom toolkits via the `experimental` option. `session.tools()` returns both remote Composio tools and your custom toolkit's tools. **TypeScript:** ```typescript import { Composio, experimental_createToolkit } from "@composio/core"; import { z } from "zod/v3"; declare const userManagement: ReturnType; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1", { experimental: { customToolkits: [userManagement], }, }); const tools = await session.tools(); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.create( user_id="user_1", experimental={ "custom_toolkits": [user_management], }, ) tools = session.tools() ``` ## Preloading custom tools [#preloading-custom-tools] Custom tools are searchable by default. Set `preload: true` / `preload=True` on a custom tool when it should be returned directly from `session.tools()`. Toolkit preload applies to all tools in that toolkit; set `preload: false` / `preload=False` on one tool to opt it out. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { OpenAIAgentsProvider } from "@composio/openai-agents"; import { z } from "zod/v3"; const composio = new Composio({ apiKey: "your_api_key", provider: new OpenAIAgentsProvider(), }); const replyGuide = experimental_createTool("GET_REPLY_STYLE_GUIDE", { name: "Get reply style guide", description: "Return the team's email reply style guide", preload: true, inputParams: z.object({ topic: z.string().describe("Email topic"), }), execute: async ({ topic }) => ({ topic, tone: "concise and helpful" }), }); const session = await composio.create("user_1", { experimental: { customTools: [replyGuide], }, }); const tools = await session.tools(); console.log(tools.map((tool) => tool.name)); // LOCAL_GET_REPLY_STYLE_GUIDE // COMPOSIO_SEARCH_TOOLS // ... other default meta tools ``` **Python:** ```python from pydantic import BaseModel, Field from composio import Composio from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) class ReplyGuideInput(BaseModel): topic: str = Field(description="Email topic") @composio.experimental.tool(preload=True) def get_reply_style_guide(input: ReplyGuideInput, ctx): """Return the team's email reply style guide.""" return {"topic": input.topic, "tone": "concise and helpful"} session = composio.create( user_id="user_1", experimental={ "custom_tools": [get_reply_style_guide], }, ) tools = session.tools() print([tool.name for tool in tools]) # LOCAL_GET_REPLY_STYLE_GUIDE # COMPOSIO_SEARCH_TOOLS # ... other default meta tools ``` ## Meta tools integration [#meta-tools-integration] Custom tools work automatically with Composio's meta tools: | Meta tool | Behavior | | ----------------------------- | ----------------------------------------------------------------------------------------------------------- | | `COMPOSIO_SEARCH_TOOLS` | Includes custom tools in search results, with slight priority for tools that don't require auth | | `COMPOSIO_GET_TOOL_SCHEMAS` | Returns schemas for custom tools alongside remote tools | | `COMPOSIO_MULTI_EXECUTE_TOOL` | Runs custom tools in-process while remote tools go to the backend, merging results transparently | | `COMPOSIO_MANAGE_CONNECTIONS` | Handles auth for extension tools. If a tool extends `gmail`, the agent can prompt the user to connect Gmail | > Custom tools are not supported in the sandbox. ## Context object (`ctx`) [#context-object-ctx] Every custom tool's `execute` function receives `(input, ctx)`. Use `ctx` to access the current user, make authenticated API requests, or call other Composio tools. **TypeScript:** | Property / Method | Description | | --------------------------------------------------------------------- | ------------------------------------------------------------- | | `ctx.userId` | The userID for the current session | | `ctx.proxyExecute({ toolkit, endpoint, method, body?, parameters? })` | Make an authenticated HTTP request via Composio's auth layer | | `ctx.execute(toolSlug, args)` | Execute any Composio native tool from within your custom tool | **Python:** | Property / Method | Description | | ------------------------------------------------------------------------ | ------------------------------------------------------------- | | `ctx.user_id` | The userID for the current session | | `ctx.proxy_execute(toolkit, endpoint, method, body=None, parameters=[])` | Make an authenticated HTTP request via Composio's auth layer | | `ctx.execute(tool_slug, arguments)` | Execute any Composio native tool from within your custom tool | See the full API in the SDK reference: [TypeScript](/reference/sdk-reference/typescript/session-context-impl) | [Python](/reference/sdk-reference/python/session-context-impl) ## Verifying registration [#verifying-registration] Use these methods to list registered tools and toolkits. Slugs include their final `LOCAL_` prefix, and toolkit-scoped tools also include the toolkit slug. **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1"); const customTools = session.customTools(); const customToolkits = session.customToolkits(); ``` **Python:** ```python custom_tools = session.custom_tools() custom_toolkits = session.custom_toolkits() ``` ## Reusing a session with custom tools [#reusing-a-session-with-custom-tools] When [reusing a session](/docs/how-composio-works#how-sessions-behave) via `composio.use()`, you can attach custom tools at the same time: **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; declare const getUserProfile: ReturnType; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.use("session_id", { customTools: [getUserProfile], }); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.use( "session_id", custom_tools=[get_user_profile], ) ``` ## Programmatic execution [#programmatic-execution] Use `session.execute()` to run custom tools directly, outside of an agent loop. Custom tools execute in-process; remote tools are sent to the backend automatically. **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1"); const result = await session.execute("GET_USER_PROFILE"); ``` **Python:** ```python result = session.execute("GET_USER_PROFILE") ``` ## Best practices [#best-practices] ### Naming and descriptions [#naming-and-descriptions] The agent relies on your tool's name and description to decide when to call it. Be specific: "Send weekly promo email" is better than "Send email". Include what the tool does, when to use it, and what it returns. In TypeScript, use uppercase slugs like `SEND_PROMO_EMAIL`. In Python, slugs are inferred from the function name, so `snake_case` produces clean defaults. You can also pass `slug` and `name` explicitly. ### Accessing authenticated APIs [#accessing-authenticated-apis] If your tool calls an API that requires user credentials (Gmail, GitHub, and so on), set `extendsToolkit` / `extends_toolkit` to the toolkit name. Composio handles authentication automatically, and the agent can prompt users to connect their account when needed. ### Defining inputs in Python [#defining-inputs-in-python] Your tool's first parameter must be a Pydantic `BaseModel`. The field descriptions become what the agent sees as the input schema, and the function's docstring becomes the tool description. You can override this by passing `description` explicitly. ### Tool names get prefixed [#tool-names-get-prefixed] Slugs exposed to the agent are automatically prefixed with `LOCAL_` and the toolkit name (if applicable): * `GET_USER_PROFILE` becomes `LOCAL_GET_USER_PROFILE` * `ASSIGN_ROLE` in `USER_MANAGEMENT` becomes `LOCAL_USER_MANAGEMENT_ASSIGN_ROLE` Your slugs cannot start with `LOCAL_`. This prefix is reserved. ## Next [#next] - [Configuring sessions](/docs/configuring-sessions): Filter toolkits and tools, set auth configs, and read tools with session.tools() --- # Shared connections (/docs/extending-sessions/shared-connections) By default, a connected account is **PRIVATE**: only the `userID` that created it can use it. A **SHARED** connection can be reached by other `userID`s, subject to a per-connection access control list (ACL). You use a shared connection by [pinning it into a session](#using-a-shared-connection): the session's `userID` doesn't own the connection, but the pin makes it available. A SHARED connection is never resolved implicitly, so a session reaches one only when you pin it explicitly. Typical use cases: * **Org-managed credentials.** One Gmail, Salesforce, or GitHub connection that every user in your app can call against, without each user having to authenticate separately. * **Background agents acting on behalf of multiple users.** The agent runs as a single service account but executes work for many `userID`s. * **Team mailboxes.** `support@` or `sales@` accounts where any teammate can send and read mail through your app. ## SHARED vs PRIVATE [#shared-vs-private] | | PRIVATE (default) | SHARED | | ---------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- | | **Who can use it** | Only the owning `userID` | The creator plus every `userID` permitted by the ACL | | **Default access for other users** | Always denied | Deny-by-default (the creator must grant access explicitly) | | **How it's used** | Implicit lookup by `userID` | Must be **explicitly pinned** in a session | | **ACL fields** | Ignored | `allowAllUsers`, `allowedUserIds`, `notAllowedUserIds` (inside the `experimental` block) | ## Creating a SHARED connection [#creating-a-shared-connection] Pass an `experimental` block to `link()` (`accountType` in TypeScript, `account_type` in Python) set to `"SHARED"`, and optionally an initial ACL. Omit the ACL block to keep the default deny-by-default state (only the creator can use it until you grant access). **Python:** ```python # Create a SHARED Gmail connection that any userId can use, # except `user_bob`. connection_request = composio.connected_accounts.link( user_id="user_admin", auth_config_id="ac_gmail_shared", experimental={ "account_type": "SHARED", "acl_config_for_shared": { "allow_all_users": True, "not_allowed_user_ids": ["user_bob"], }, }, ) print(connection_request.redirect_url) # Have user_admin complete the OAuth flow at the redirect URL, # then wait for the connection to become ACTIVE. connected = connection_request.wait_for_connection() print(f"Shared connection ready: {connected.id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Create a SHARED Gmail connection that any userId can use, // except `user_bob`. const connectionRequest = await composio.connectedAccounts.link( "user_admin", "ac_gmail_shared", { experimental: { accountType: "SHARED", aclConfigForShared: { allowAllUsers: true, notAllowedUserIds: ["user_bob"], }, }, }, ); console.log(connectionRequest.redirectUrl); // Have user_admin complete the OAuth flow at the redirect URL, // then wait for the connection to become ACTIVE. const connected = await connectionRequest.waitForConnection(); console.log(`Shared connection ready: ${connected.id}`); ``` The returned `connectedAccountId` (`ca_...`) is the ID you'll pin into other users' sessions. > ACL fields are only valid on SHARED connections. Sending an `experimental.acl_config_for_shared` block on a PRIVATE connection raises `ComposioAclOnlyForSharedError`. ## Using a shared connection [#using-a-shared-connection] Pin the SHARED connection into a session through `connectedAccounts`. The session belongs to a *different* `userID` than the creator, and the pin is what makes the SHARED connection visible to that session. The session config itself is **not** experimental. You pin the connection by ID exactly as you would a PRIVATE one. **Python:** ```python # user_alice starts a session that pins the shared Gmail connection. # Gmail tools loaded from this session will resolve to that connection # even though user_alice did not create it. session = composio.sessions.create( user_id="user_alice", connected_accounts={ "gmail": ["ca_gmail_shared"], }, ) tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // user_alice starts a session that pins the shared Gmail connection. // Gmail tools loaded from this session will resolve to that connection // even though user_alice did not create it. const session = await composio.create("user_alice", { connectedAccounts: { gmail: ["ca_gmail_shared"], }, }); const tools = await session.tools(); ``` > A session may pin **at most one SHARED connection per toolkit**. Pinning two SHARED Gmail connections in the same session is rejected at session create time. Mixing one SHARED with multiple PRIVATE pins is allowed. ## ACL resolution rule [#acl-resolution-rule] When a non-creator `userID` attempts to use a SHARED connection, the ACL is evaluated in this order: 1. `userId` ∈ `notAllowedUserIds` → **DENY** 2. `allowAllUsers === true` → **ALLOW** 3. `userId` ∈ `allowedUserIds` → **ALLOW** 4. otherwise → **DENY** *(deny-by-default)* Deny wins on conflict, which lets you express *"share with everyone except a few people"* by setting `allowAllUsers: true` and naming the exceptions in `notAllowedUserIds`. The creator can always use their own connection. The ACL only governs other `userID`s. ### Common ACL patterns [#common-acl-patterns] The table below shows the inner shape of the ACL block (`aclConfigForShared` in TypeScript, `acl_config_for_shared` in Python). Wrap it inside the `experimental` block at the call site. Field names are camelCase in the TypeScript samples; Python callers translate to snake\_case (`allow_all_users`, `allowed_user_ids`, `not_allowed_user_ids`). | Goal | ACL block | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Only the creator** (default) | `{}` (or omit the block) | | **Allow every `userId`** | `{ allowAllUsers: true }` | | **Targeted allow list** | `{ allowedUserIds: ["user_alice", "user_bob"] }` | | **Everyone except a few users** | `{ allowAllUsers: true, notAllowedUserIds: ["user_bob"] }` | | **Combined: open + targeted deny** | `{ allowAllUsers: true, notAllowedUserIds: ["user_bob"], allowedUserIds: ["user_alice"] }` (Bob still denied, deny wins) | Each list accepts up to 1000 entries; each `userID` is 1..256 characters. ## Updating the ACL [#updating-the-acl] Call `updateAcl()` on the connected accounts namespace to change access after creation. It follows PATCH semantics: pass only the fields you want to change, omit a field to leave it unchanged, and pass an empty array to clear an allow or deny list. **Python:** ```python # Open access to everyone. composio.connected_accounts.update_acl( "ca_gmail_shared", allow_all_users=True, ) # Add a targeted allow list (without touching the wildcard or deny list). composio.connected_accounts.update_acl( "ca_gmail_shared", allowed_user_ids=["user_alice", "user_bob"], ) # Revoke the allow list; only the creator can use it again # (unless allow_all_users is True). composio.connected_accounts.update_acl( "ca_gmail_shared", allowed_user_ids=[], ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Open access to everyone. await composio.connectedAccounts.updateAcl("ca_gmail_shared", { allowAllUsers: true, }); // Add a targeted allow list (without touching the wildcard or deny list). await composio.connectedAccounts.updateAcl("ca_gmail_shared", { allowedUserIds: ["user_alice", "user_bob"], }); // Revoke the allow list; only the creator can use it again // (unless allowAllUsers is true). await composio.connectedAccounts.updateAcl("ca_gmail_shared", { allowedUserIds: [], }); ``` > Passing `notAllowedUserIds: []` **clears the deny list**, which silently re-grants access to users you previously blocked. Always audit the allow side when clearing a deny list. ACL writes are restricted to the connection's creator or an API key caller. Other callers get a permission error. ## Listing SHARED connections [#listing-shared-connections] By default `list()` returns **PRIVATE only**, so shared accounts must be requested explicitly. Pass an `account_type` (Python) or `accountType` (TypeScript) filter to scope the query. | Value | Returns | | ---------------------------------------------- | ------------------------ | | `'PRIVATE'` *(default when omitted)* | Only PRIVATE connections | | `'SHARED'` | Only SHARED connections | | `'ALL'` | PRIVATE + SHARED | The filter is a flat query param on the wire (`?account_type=...`), so it stays flat in both SDKs, unlike the create and update surfaces, which nest under `experimental`. **Python:** ```python # List every SHARED connection the caller has visibility into. shared = composio.connected_accounts.list(account_type="SHARED") for item in shared.items: print(item.id, item.toolkit.slug) # Scope to a single user's SHARED connections. shared_for_alice = composio.connected_accounts.list( account_type="SHARED", user_ids=["user_alice"], ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // List every SHARED connection the caller has visibility into. const shared = await composio.connectedAccounts.list({ accountType: "SHARED" }); for (const item of shared.items) { console.log(item.id, item.toolkit.slug); } // Scope to a single user's SHARED connections. const sharedForAlice = await composio.connectedAccounts.list({ accountType: "SHARED", userIds: ["user_alice"], }); ``` ## Inspecting the ACL [#inspecting-the-acl] `get()` and `list()` responses surface `accountType` and `aclConfigForShared` under the same `experimental` block as the request shape. The `aclConfigForShared` field is populated only when the caller is the connection's creator or is using an API key. Other callers see the `experimental` block without that field. **Python:** ```python account = composio.connected_accounts.get("ca_gmail_shared") if account.experimental: print(f"Type: {account.experimental.account_type}") # "PRIVATE" or "SHARED" if account.experimental.acl_config_for_shared: acl = account.experimental.acl_config_for_shared print(f"Allow all users: {acl.allow_all_users}") print(f"Allowed: {acl.allowed_user_ids}") print(f"Denied: {acl.not_allowed_user_ids}") else: # You're not authorised to see the ACL on this connection. print("ACL hidden") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const account = await composio.connectedAccounts.get("ca_gmail_shared"); if (account.experimental) { console.log("Type:", account.experimental.accountType); // "PRIVATE" or "SHARED" if (account.experimental.aclConfigForShared) { const acl = account.experimental.aclConfigForShared; console.log("Allow all users:", acl.allowAllUsers); console.log("Allowed:", acl.allowedUserIds); console.log("Denied:", acl.notAllowedUserIds); } else { // You're not authorised to see the ACL on this connection. console.log("ACL hidden"); } } ``` ## Error handling [#error-handling] | Error | When | | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ComposioAclOnlyForSharedError` (400) | ACL fields sent on a PRIVATE connection (at create or update time). | | `ComposioSharedAccessDeniedError` (403) | Direct execute with a SHARED `connectedAccountId` that the requesting `userId` isn't permitted to use. | | `ComposioSharedConnectionNotAccessibleError` (400) | A session pinned a SHARED connection that the session's `userID` cannot use. The error is raised at session create time, so the session never enters a state that fails mid-execution. | The access errors are caught the same way as any other Composio exception (`ComposioAclOnlyForSharedError` and `ComposioSharedAccessDeniedError` are exported from `@composio/core`, and live under `composio.exceptions` in Python). ## Next [#next] - [Configuring sessions](/docs/configuring-sessions): Pin connected accounts, auth configs, and toolkit restrictions into a session --- # Custom MCP (/docs/extending-sessions/custom-mcp) Custom MCP lets you use tools from your remote MCP server in the same session as Composio's built-in toolkits. Register the server, connect it if authentication is required, then add its synced `CUSTOM_*` toolkit slug to a session. > Custom MCP is experimental. Its setup flow, authentication options, and API contracts may change while we work with early customers. This is different from [Custom Tools and Toolkits](/docs/extending-sessions/custom-tools-and-toolkits). Custom tools run inside your application. A custom MCP server runs outside Composio and exposes its tools over a public HTTPS endpoint. ## Custom MCP lifecycle [#custom-mcp-lifecycle] A Custom MCP moves through this lifecycle: 1. **Deploy** your MCP server at a public HTTPS URL. 2. **Register** its URL and authentication scheme. Composio creates a project-scoped `CUSTOM_*` toolkit. 3. **Connect** an account if the server uses an API key or DCR OAuth. No-auth servers skip this step. 4. **Sync** its tools. The first sync starts automatically; later tool changes require a manual sync. 5. **Use** the toolkit in a session. For authenticated servers, explicitly select the connected account. > **API-only while Custom MCP is experimental**: Use the lifecycle endpoints below to register, sync, and delete Custom MCP toolkits. The SDKs don't expose these endpoints yet, and their contracts may change while Custom MCP is experimental. The API reference has the full request and response schemas for [upsert](/reference/api-reference/toolkits/postCustomToolkitsUpsert), [sync](/reference/api-reference/toolkits/postCustomToolkitsSync), and [delete](/reference/api-reference/toolkits/deleteCustomToolkitsBySlug). Dashboard management is coming soon for customers who prefer a UI. ## Register a Custom MCP [#register-a-custom-mcp] Call `POST /api/v3.1/custom/toolkits/upsert` with your public server URL and authentication scheme. Authenticate the request with your Composio project API key. **No auth:** ```bash curl --request POST \ --url https://backend.composio.dev/api/v3.1/custom/toolkits/upsert \ --header "x-api-key: $COMPOSIO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "slug": "ACME", "toolkit_config": { "name": "Acme", "app_url": "https://mcp.example.com/mcp", "auth_schemes": [ { "mode": "NO_AUTH" } ] } }' ``` **API key:** ```bash curl --request POST \ --url https://backend.composio.dev/api/v3.1/custom/toolkits/upsert \ --header "x-api-key: $COMPOSIO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "slug": "ACME", "toolkit_config": { "name": "Acme", "app_url": "https://mcp.example.com/mcp", "auth_schemes": [ { "mode": "API_KEY", "headers": { "Authorization": "Bearer {{generic_api_key}}" } } ] } }' ``` `generic_api_key` is replaced with the credential stored on the connected account. You can use a different header name or value format, but at least one header value must contain `{{generic_api_key}}`. **DCR OAuth:** ```bash curl --request POST \ --url https://backend.composio.dev/api/v3.1/custom/toolkits/upsert \ --header "x-api-key: $COMPOSIO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "slug": "ACME", "toolkit_config": { "name": "Acme", "app_url": "https://mcp.example.com/mcp", "auth_schemes": [ { "mode": "DCR_OAUTH", "discovery_url": "https://mcp.example.com/.well-known/oauth-authorization-server" } ] } }' ``` Composio adds the `CUSTOM_` prefix and returns the normalized toolkit slug: ```json { "slug": "CUSTOM_ACME" } ``` > **app_url and auth_schemes are immutable**: Re-registering a slug your project already owns updates the toolkit in place: mutable fields like the name and logo take the new values, and an identical config is a harmless no-op. Two fields can't change after registration: `app_url` and `auth_schemes`. Changing either returns `409 Conflict`; [delete the existing toolkit](#delete-or-replace-a-custom-mcp), then register it again. ## Add a toolkit logo [#add-a-toolkit-logo] Without a logo, your toolkit shows the Composio logo in the dashboard and on end-user connect screens. To ship your own branding, include `toolkit_config.logo_file` when you register: the image itself, base64-encoded. Composio validates it, stores it on Composio-hosted asset storage, and renders it everywhere the toolkit appears. You don't host anything, and the logo keeps working even if your own site changes or goes down. ```bash curl --request POST \ --url https://backend.composio.dev/api/v3.1/custom/toolkits/upsert \ --header "x-api-key: $COMPOSIO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "slug": "ACME", "toolkit_config": { "name": "Acme", "app_url": "https://mcp.example.com/mcp", "logo_file": { "content": "iVBORw0KGgoAAAANSUhEUgAA...", "mime_type": "image/png" }, "auth_schemes": [ { "mode": "NO_AUTH" } ] } }' ``` Set `content` to your image encoded as base64: a single line, with no line breaks or whitespace. The image must be: * **PNG or JPEG** (`mime_type` of `image/png` or `image/jpeg`) * **Square**, between 256 and 1024 pixels * **At most 3MB** before encoding Omit `logo_file` to keep the Composio default. To change a logo later, re-register the same slug with the new image: the upsert updates the toolkit in place. ## Complete setup for your authentication mode [#complete-setup-for-your-authentication-mode] Choose the mode that matches your server, then complete any required connection: | Authentication | Use it when | After registration | | -------------- | ------------------------------------------------------ | ------------------------------------------------------------------------ | | No auth | The server accepts requests without credentials. | No connection is required. Initial sync runs automatically. | | API key | Each connected account supplies an API key. | Create an active connection. Initial sync then starts in the background. | | DCR OAuth | The server supports OAuth Dynamic Client Registration. | Authorize a connection. Initial sync starts when it becomes active. | For DCR OAuth, the server must support the standard authorization-code flow. Other OAuth grant types aren't supported. ### Create the auth config for automatic account matching [#create-the-auth-config-for-automatic-account-matching] Registering a toolkit does not create an auth config. For API-key and DCR OAuth servers, creating one yourself is a *required* separate step: without an auth config there is nothing for end users to connect to, and the toolkit's tools can't authenticate. Create it right after registration, with `is_enabled_for_tool_router` set to `true` so sessions can match connected accounts by `user_id`: ```bash curl --request POST \ --url https://backend.composio.dev/api/v3.1/auth_configs \ --header "x-api-key: $COMPOSIO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "toolkit": { "slug": "CUSTOM_ACME" }, "auth_config": { "type": "use_custom_auth", "authScheme": "API_KEY", "credentials": {}, "is_enabled_for_tool_router": true } }' ``` This flag is what lets sessions find the toolkit's connected accounts by `user_id` automatically. Without it, session executions fail with `NoActiveConnection` even when an active account exists, and you must [select the account explicitly](#use-an-authenticated-server) in every session. If you already created the config without the flag, patch it: ```bash curl --request PATCH \ --url https://backend.composio.dev/api/v3.1/auth_configs/ac_xxxxxxxx \ --header "x-api-key: $COMPOSIO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "type": "custom", "is_enabled_for_tool_router": true }' ``` ## Sync and resync tools [#sync-and-resync-tools] Call `POST /api/v3.1/custom/toolkits/sync` to fetch the server's current tools: ```bash curl --request POST \ --url https://backend.composio.dev/api/v3.1/custom/toolkits/sync \ --header "x-api-key: $COMPOSIO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "slug": "CUSTOM_ACME", "connected_account_id": "ca_custom_acme" }' ``` For an API-key or DCR OAuth server, `connected_account_id` must identify an active account from the same toolkit and project. For a no-auth server, omit it: ```bash curl --request POST \ --url https://backend.composio.dev/api/v3.1/custom/toolkits/sync \ --header "x-api-key: $COMPOSIO_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "slug": "CUSTOM_ACME" }' ``` A successful sync returns the toolkit version and the number of tools discovered: ```json { "slug": "CUSTOM_ACME", "version": "20260728_00", "synced_count": 12 } ``` > **When to sync manually**: Composio starts the initial sync automatically: * **No auth:** during registration * **API key or DCR OAuth:** when the first connected account becomes active Call the sync endpoint only if the initial sync fails or the server's tool definitions change. Later connections don't resync a toolkit that already has tools. A Custom MCP toolkit can contain at most 500 tools. If the server returns more than 500, the sync fails without importing a partial tool list. The last successful version stays available. ## Delete or replace a Custom MCP [#delete-or-replace-a-custom-mcp] Re-registering a slug updates mutable fields like the name and logo, but `app_url` and `auth_schemes` can't change after registration. To replace either, delete the toolkit and register it again. Call `DELETE /api/v3.1/custom/toolkits/{slug}`: ```bash curl --request DELETE \ --url https://backend.composio.dev/api/v3.1/custom/toolkits/CUSTOM_ACME \ --header "x-api-key: $COMPOSIO_API_KEY" ``` ```json { "slug": "CUSTOM_ACME", "deleted": true, "revoke_job_ids": ["job_123"], "auth_configs_soft_deleted": 1, "connected_accounts_soft_deleted": 1 } ``` > **Deletion also removes connections**: Deletion removes the custom toolkit and its tools. It also revokes and removes the toolkit's auth configurations and connected accounts. Any replacement starts with new connections. ## Use Custom MCP in a session [#use-custom-mcp-in-a-session] Once the toolkit is synced, add its `CUSTOM_*` slug to a session. ### Use a no-auth server [#use-a-no-auth-server] Pass the toolkit slug when you create the session: **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.sessions.create( user_id="user_123", toolkits=["CUSTOM_ACME"], ) tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.sessions.create("user_123", { toolkits: ["CUSTOM_ACME"], }); const tools = await session.tools(); ``` With the default search-first session, your agent can discover the custom tools through `COMPOSIO_SEARCH_TOOLS` and execute them through the Tool Router. ### Use an authenticated server [#use-an-authenticated-server] Sessions match a connected account by `user_id` automatically when the toolkit's auth config was [created with `is_enabled_for_tool_router: true`](#create-the-auth-config-for-automatic-account-matching). If your auth config doesn't have that flag, explicitly select the connected account in the session config instead: **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.sessions.create( user_id="user_123", toolkits=["CUSTOM_ACME"], connected_accounts={ "CUSTOM_ACME": ["ca_custom_acme"], }, ) ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.sessions.create("user_123", { toolkits: ["CUSTOM_ACME"], connectedAccounts: { CUSTOM_ACME: ["ca_custom_acme"], }, }); ``` The pinned account must belong to the custom toolkit and be active. Explicit selection ensures tool calls use its credentials. ## What you manage and what Composio handles [#what-you-manage-and-what-composio-handles] | Area | You manage | Composio handles | | -------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | | Server | Deploying and operating the remote MCP server at a public HTTPS URL. | Connecting to the URL for tool discovery and execution. Composio does not host the server. | | Tools | Implementing tools and deciding when later definition changes are ready to sync. | Starting the initial sync, importing tool schemas, versioning them, and exposing them to sessions. | | Authentication | Implementing the server's API-key or DCR OAuth behavior and completing each required connection. | Storing connected-account credentials and sending them when discovering or calling tools. | | Lifecycle | Choosing when to resync, delete, or replace the toolkit. | Providing the project-scoped registration, sync, and deletion operations. | ## Technical behavior [#technical-behavior] Each registered server becomes a custom toolkit scoped to your project: * The toolkit has `type: "custom"` and the `CUSTOM` category. * Its slug starts with `CUSTOM_`, such as `CUSTOM_ACME`. * Its tools are available through Tool Router search and toolkit-filtered tool listing. * Tool execution is proxied to your MCP server with the selected connected account's credentials. On v3, `GET /api/v3/tools?toolkit_slug=CUSTOM_ACME` can return an empty list even after a successful sync. This is *not* a sync failure: v3 reads from a pinned toolkit version by default, and custom tools only exist in the latest version. Add `toolkit_versions=latest` to the request, or use v3.1, which always resolves the latest version: ```bash curl --request GET \ --url "https://backend.composio.dev/api/v3/tools?toolkit_slug=CUSTOM_ACME&toolkit_versions=latest" \ --header "x-api-key: $COMPOSIO_API_KEY" ``` Custom toolkits use dated registry versions. The v3.1 tools API selects the latest version by default. If you use v3 directly, select the latest version for each operation: | v3 operation | Select the latest version | | ----------------- | -------------------------------------------------- | | List tools | Add the `toolkit_versions=latest` query parameter. | | Retrieve one tool | Add the `version=latest` query parameter. | | Execute one tool | Set `"version": "latest"` in the request body. | See [Toolkit Versioning](/docs/tools-direct/toolkit-versioning) for more examples. ## Known gaps [#known-gaps] **Setup and lifecycle** * **API-only setup:** The SDKs don't expose registration, sync, update, or deletion methods yet. Use the lifecycle endpoints on this page, then use the toolkit slug through the SDK. * **Dashboard coming soon:** Custom MCP management isn't available in the dashboard yet. * **Remote servers only:** Composio doesn't host your server. Deploy it at a public HTTPS endpoint; local and STDIO-only servers aren't supported. * **Immutable `app_url` and `auth_schemes`:** Re-registering an existing slug updates other fields in place, but changing `app_url` or `auth_schemes` returns `409 Conflict`. Delete the toolkit, then register it again. Deletion also removes its auth configurations and connected accounts. * **500-tool limit:** Split larger servers into smaller MCP servers. If a later sync exceeds the limit, the last successful version stays available. **Sync and authentication** * **No continuous sync:** Auto-sync only populates an empty toolkit during registration or the first active connection. If it fails, call the sync endpoint with an active connected account. Sync again whenever tool definitions change. * **API-key validation is limited:** Setup checks that a key was provided, not that the remote server accepts it. Run a safe tool after connecting to verify the credential end to end. **Sessions and tool APIs** * **Automatic account matching requires a flag:** sessions only match a custom toolkit's connected accounts when its auth config has `is_enabled_for_tool_router: true`. Set it at creation (or PATCH it in later); otherwise pass the account ID through `connected_accounts` in Python or `connectedAccounts` in TypeScript. * **Prefer the v3.1 tools API:** The v3 tools API pins a default version that doesn't contain custom tools. If you must use v3, explicitly select `latest` as described above. ## Related guides [#related-guides] - [Using sessions via MCP](/docs/sessions-via-mcp): Connect an MCP client to a Composio-hosted session - [Custom Tools and Toolkits](/docs/extending-sessions/custom-tools-and-toolkits): Run your own tools inside your application process - [Configuring Sessions](/docs/configuring-sessions): Configure toolkit and tool filtering - [Managing Multiple Connected Accounts](/docs/authentication/managing-multiple-connected-accounts): Select a connected account explicitly --- # Creating triggers (/docs/setting-up-triggers/creating-triggers) A trigger watches for one event (like `GITHUB_COMMIT_EVENT`) on one user's connected account. Create one, and events start flowing to your [subscription or webhook URL](/docs/setting-up-triggers/subscribing-to-events). For the bigger picture, see [Triggers](/docs/triggers). The user needs a [connected account](/docs/authentication) for the toolkit you want to monitor. See [Authentication](/docs/authentication) if you haven't set that up. ## Inspect the trigger type [#inspect-the-trigger-type] Each trigger type declares the config it needs. Check it before you create, so you pass the right fields. **Python:** ```python from composio import Composio composio = Composio() trigger_type = composio.triggers.get_type("GITHUB_COMMIT_EVENT") print(trigger_type.config) # {"properties": {"owner": {...}, "repo": {...}}, "required": ["owner", "repo"]} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const triggerType = await composio.triggers.getType('GITHUB_COMMIT_EVENT'); console.log(triggerType.config); // {"properties": {"owner": {...}, "repo": {...}}, "required": ["owner", "repo"]} ``` ## Create the trigger [#create-the-trigger] Pass the user, the trigger slug, and the config the type requires. **Python:** ```python from composio import Composio composio = Composio() user_id = "user-id-123435" # The user needs a connected account for this toolkit before this runs. # Set it up first. See /docs/authentication. trigger = composio.triggers.create( slug="GITHUB_COMMIT_EVENT", user_id=user_id, trigger_config={"owner": "your-repo-owner", "repo": "your-repo-name"}, ) print(f"Trigger created: {trigger.trigger_id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const userId = 'user-id-123435'; // The user needs a connected account for this toolkit before this runs. // Set it up first. See /docs/authentication. const trigger = await composio.triggers.create(userId, 'GITHUB_COMMIT_EVENT', { triggerConfig: { owner: 'your-repo-owner', repo: 'your-repo-name' }, }); console.log(`Trigger created: ${trigger.triggerId}`); ``` You only pass a `user_id`. Composio resolves that user's connected account for the toolkit automatically. ### Targeting a specific connected account [#targeting-a-specific-connected-account] If a user has more than one connected account for the toolkit, Composio uses the first active connection for the user and the trigger's toolkit. Pass a connected account ID to pick exactly which account the trigger watches. **Python:** ```python trigger = composio.triggers.create( slug="GITHUB_COMMIT_EVENT", user_id=user_id, connected_account_id="ca_def456", trigger_config={"owner": "your-repo-owner", "repo": "your-repo-name"}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const userId = 'user-id-123435'; const trigger = await composio.triggers.create(userId, 'GITHUB_COMMIT_EVENT', { connectedAccountId: 'ca_def456', triggerConfig: { owner: 'your-repo-owner', repo: 'your-repo-name' }, }); ``` Trigger instances default to the `'latest'` toolkit version. If you parse payloads against a fixed schema, [pin a version](/docs/tools-direct/toolkit-versioning#choosing-between-latest-and-a-pinned-version) at SDK initialization. That's it. The trigger is active. Next, [receive its events](/docs/setting-up-triggers/subscribing-to-events). ## Next [#next] - [Receiving events](/docs/setting-up-triggers/subscribing-to-events): Get trigger events locally with the SDK or in production over your webhook URL --- # Receiving events (/docs/setting-up-triggers/subscribing-to-events) Once a trigger is active, its events come to you as the same payload, whether you're testing locally or running in production. Develop against your local handler first, then point Composio at your production URL when you ship. ## Receive events locally [#receive-events-locally] While developing, you want trigger events on your machine. The best option forwards them to the real webhook handler you'll run in production, so you test the exact path (including `parse()` and signature verification) before you ship. ### Quick look with `subscribe()` [#quick-look-with-subscribe] `subscribe()` streams events straight to your process over a WebSocket, with no webhook URL, no tunnel, and no signing. It's the fastest way to eyeball what a trigger sends, but it bypasses your real webhook handler. Use it only for basic prototyping; for anything you intend to ship, forward events to your handler with one of the options below. > Under the hood `subscribe()` opens the WebSocket via [Pusher](https://pusher.com/). This is an implementation detail, but worth knowing if your runtime restricts WebSocket clients — prefer the webhook/forwarding options below for anything you ship. **Python:** ```python from composio import Composio composio = Composio() subscription = composio.triggers.subscribe() @subscription.handle(trigger_id="your_trigger_id") def handle_event(data): print(f"Event received: {data}") subscription.wait_forever() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); await composio.triggers.subscribe( data => { console.log('Event received:', data); }, { triggerId: 'your_trigger_id' } ); ``` Filter the stream by `triggerId`, `triggerSlug`, `connectedAccountId`, `toolkits`, or `userId`, or pass no filters to receive every trigger event in the project. ### Forward to your local handler with the CLI (recommended) [#forward-to-your-local-handler-with-the-cli-recommended] The Composio CLI streams realtime events and forwards each one to your local URL, signed exactly like production. No public URL, no tunnel, and it runs your real handler (and [`parse()`](#handling-events)) end to end. ```bash composio dev triggers listen --forward "http://localhost:8000/webhooks/composio" ``` Events are signed with `COMPOSIO_WEBHOOK_SECRET` if it's set, otherwise the CLI prints a generated secret to verify against. Filter the stream with `--toolkits`, `--trigger-slug`, or `--trigger-id`, and tee events to a file with `--out events.jsonl`. ### Cloudflare Tunnel [#cloudflare-tunnel] Expose your local server with [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), no account needed for quick runs: ```bash cloudflared tunnel --url http://localhost:8000 ``` Register the printed `trycloudflare.com` URL as your [webhook URL](#receive-events-in-production), then events reach your handler at `http://localhost:8000/webhooks/composio`. ### ngrok [#ngrok] Expose your local server with [ngrok](https://ngrok.com): ```bash ngrok http 8000 ``` Register the printed `ngrok-free.app` URL as your [webhook URL](#receive-events-in-production) the same way. ## Receive events in production [#receive-events-in-production] Register your webhook URL once per project. Composio then `POST`s every trigger event to it. Set it from the SDK: **Python:** ```python from composio import Composio composio = Composio() subscription = composio.triggers.set_webhook_subscription( webhook_url="https://your-app.com/webhooks/composio", ) print(f"Delivering events to {subscription['webhook_url']}") # Store subscription['secret'] to verify signatures ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const subscription = await composio.triggers.setWebhookSubscription({ webhookUrl: 'https://your-app.com/webhooks/composio', }); console.log(`Delivering events to ${subscription.webhookUrl}`); // Store subscription.secret to verify signatures ``` Your webhook endpoint must be publicly reachable. Composio's outbound IPs are dynamic, so IP allowlists and VPN-only endpoints won't work. Authenticate payloads with [signature verification](#verifying-signatures) instead. ## Handling events [#handling-events] In your handler, pass the incoming request to `parse()`. It returns the typed, normalized payload. Pass `verifySecret` and it verifies the signature first, so one call both authenticates and parses. **Python:** ```python import os from composio import Composio composio = Composio() @app.post("/webhooks/composio") async def webhook_handler(request: Request): # On async frameworks (FastAPI) read the raw body and pass body=/headers=. # On sync frameworks (Flask, Django) you can pass the request directly. # Use the raw body so the signature verifies. Omit verify_secret to skip it. result = composio.triggers.parse( body=await request.body(), headers=request.headers, verify_secret=os.environ["COMPOSIO_WEBHOOK_SECRET"], ) if result["raw_payload"]["type"] == "composio.trigger.message": event = result["payload"] if event["trigger_slug"] == "GITHUB_COMMIT_EVENT": data = event["payload"] print(f"New commit by {data['author']}: {data['message']}") return {"status": "ok"} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); // Next.js App Router, Hono, or any Fetch-style handler export async function POST(request: Request) { // parse() takes a Fetch Request (shown) or an Express-style { body, headers }. // Pass the raw body so the signature verifies; omit verifySecret to skip it. const result = await composio.triggers.parse(request, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); if (result.rawPayload.type === 'composio.trigger.message') { const event = result.payload; if (event.triggerSlug === 'GITHUB_COMMIT_EVENT') { const data = event.payload; console.log(`New commit by ${data.author}: ${data.message}`); } } return Response.json({ status: 'ok' }); } ``` > Composio delivers other project events (like [connection expiry](/docs/authentication#connection-lifecycle)) to this same URL. `parse()` returns those too. Route on `result.payload.triggerSlug` and ignore what you don't handle. ### Inspecting trigger payload schemas [#inspecting-trigger-payload-schemas] Each trigger type declares the shape of the `data` it sends. Inspect it before you write your handler: **Python:** ```python from composio import Composio composio = Composio() trigger_type = composio.triggers.get_type("GITHUB_COMMIT_EVENT") print(trigger_type.payload) # {"properties": {"author": {...}, "id": {...}, "message": {...}, "timestamp": {...}, "url": {...}}} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const triggerType = await composio.triggers.getType('GITHUB_COMMIT_EVENT'); console.log(triggerType.payload); // {"properties": {"author": {...}, "id": {...}, "message": {...}, "timestamp": {...}, "url": {...}}} ``` From the CLI, inspect a single trigger type's config and payload schema: ```bash composio triggers info "GITHUB_COMMIT_EVENT" ``` Or generate typed stubs for your project (scope to the toolkits you use with `--toolkits`), so your handler is type-checked against the trigger's payload: ```bash composio generate --toolkits github # auto-detects TypeScript or Python ``` ### Webhook payload shape [#webhook-payload-shape] Every trigger event arrives in the same envelope. `metadata` tells you where the event came from; `data` holds the event itself, in the shape the trigger type declares. ```json { "id": "msg_abc123", "type": "composio.trigger.message", "metadata": { "log_id": "log_abc123", "trigger_slug": "GITHUB_COMMIT_EVENT", "trigger_id": "ti_xyz789", "connected_account_id": "ca_def456", "auth_config_id": "ac_xyz789", "user_id": "user-id-123435" }, "data": { "commit_sha": "a1b2c3d", "message": "fix: resolve null pointer", "author": "jane" }, "timestamp": "2026-01-15T10:30:00Z" } ``` | `metadata` field | What it tells you | | ---------------------- | ---------------------------------------------------- | | `trigger_id` | Which trigger instance fired this event | | `trigger_slug` | The trigger type (for example `GITHUB_COMMIT_EVENT`) | | `connected_account_id` | Which connected account it belongs to | | `user_id` | Which user it's for | | `auth_config_id` | Which auth config was used | > This is the V3 payload, the default for new organizations. See [webhook payload versions](#webhook-payload-versions) for V2 and V1. ## Verifying signatures [#verifying-signatures] Composio signs every webhook request. `parse({ verifySecret })` verifies the signature for you (and `verifyWebhook()` does the same at a lower level), so most handlers need nothing more. You only need this section if you're **not** using the Composio SDK. > Store the webhook secret securely as `COMPOSIO_WEBHOOK_SECRET`. Fetch it from the [webhook subscription](/reference/api-reference/webhook-subscriptions/getWebhookSubscriptionsById) any time, or [rotate it](/reference/api-reference/webhook-subscriptions/postWebhookSubscriptionsByIdRotateSecret) if it leaks. Every request includes `webhook-signature`, `webhook-id`, and `webhook-timestamp` headers. Compute `HMAC-SHA256` over `{webhook-id}.{webhook-timestamp}.{rawBody}` with your secret and compare it against the signature: **Python:** ```python import hmac import hashlib import base64 import json import os def verify_webhook(webhook_id: str, webhook_timestamp: str, body: str, signature: str) -> dict: secret = os.getenv("COMPOSIO_WEBHOOK_SECRET", "") signing_string = f"{webhook_id}.{webhook_timestamp}.{body}" expected = base64.b64encode( hmac.new(secret.encode(), signing_string.encode(), hashlib.sha256).digest() ).decode() received = signature.split(",", 1)[1] if "," in signature else signature if not hmac.compare_digest(expected, received): raise ValueError("Invalid webhook signature") payload = json.loads(body) # V3 payload return { "trigger_slug": payload["metadata"]["trigger_slug"], "data": payload["data"], } ``` **TypeScript:** ```typescript import crypto from 'crypto'; function verifyWebhook( webhookId: string, webhookTimestamp: string, body: string, signature: string ) { const secret = process.env.COMPOSIO_WEBHOOK_SECRET ?? ''; const signingString = `${webhookId}.${webhookTimestamp}.${body}`; const expected = crypto .createHmac('sha256', secret) .update(signingString) .digest('base64'); const received = signature.split(',')[1] ?? signature; if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) { throw new Error('Invalid webhook signature'); } const payload = JSON.parse(body); // V3 payload return { triggerSlug: payload.metadata.trigger_slug, data: payload.data, }; } ``` > Reject requests whose `webhook-timestamp` is too old to block replays. The SDK's `parse()` and `verifyWebhook()` enforce a 300-second tolerance by default; pass `tolerance` to change it, or `0` to disable the check. ## Webhook payload versions [#webhook-payload-versions] `parse()` and `verifyWebhook()` auto-detect the version. If you process payloads manually, here are the formats: **V3 (default):** Metadata is separated from event data. New organizations receive V3 payloads by default. ```json { "id": "msg_abc123", "type": "composio.trigger.message", "metadata": { "log_id": "log_abc123", "trigger_slug": "GITHUB_COMMIT_EVENT", "trigger_id": "ti_xyz789", "connected_account_id": "ca_def456", "auth_config_id": "ac_xyz789", "user_id": "user-id-123435" }, "data": { "commit_sha": "a1b2c3d", "message": "fix: resolve null pointer", "author": "jane" }, "timestamp": "2026-01-15T10:30:00Z" } ``` **V2 (legacy):** Metadata fields are mixed into the `data` object alongside event data. ```json { "type": "github_commit_event", "data": { "commit_sha": "a1b2c3d", "message": "fix: resolve null pointer", "author": "jane", "connection_id": "ca_def456", "connection_nano_id": "cn_abc123", "trigger_nano_id": "tn_xyz789", "trigger_id": "ti_xyz789", "user_id": "user-id-123435" }, "timestamp": "2026-01-15T10:30:00Z", "log_id": "log_abc123" } ``` **V1 (legacy):** ```json { "trigger_name": "github_commit_event", "trigger_id": "ti_xyz789", "connection_id": "ca_def456", "payload": { "commit_sha": "a1b2c3d", "message": "fix: resolve null pointer", "author": "jane" }, "log_id": "log_abc123" } ``` ## Next [#next] - [Managing triggers](/docs/setting-up-triggers/managing-triggers): List, enable, disable, and delete trigger instances --- # Managing triggers (/docs/setting-up-triggers/managing-triggers) After a trigger is created, you manage it over its lifecycle: list active instances, pause one with `disable()`, bring it back with `enable()`, or remove it for good with `delete()`. ## Listing active triggers [#listing-active-triggers] List the trigger instances you've created. Results are cursor-paginated. **Python:** ```python from composio import Composio composio = Composio() active = composio.triggers.list_active( connected_account_ids=["ca_def456"], ) for trigger in active.items: print(f"{trigger.id} ({trigger.trigger_name}) - disabled: {trigger.disabled_at is not None}") # Paginate with cursor if active.next_cursor: next_page = composio.triggers.list_active(cursor=active.next_cursor) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const active = await composio.triggers.listActive({ connectedAccountIds: ['ca_def456'], }); for (const trigger of active.items) { console.log(`${trigger.id} (${trigger.triggerName}) - disabled: ${trigger.disabledAt !== null}`); } // Paginate with cursor if (active.nextCursor) { const nextPage = await composio.triggers.listActive({ cursor: active.nextCursor }); } ``` | Filter | Description | | ----------------------------------------------- | -------------------------------------------- | | `connected_account_ids` / `connectedAccountIds` | Array of connected account IDs | | `trigger_ids` / `triggerIds` | Array of trigger instance IDs | | `trigger_names` / `triggerNames` | Array of trigger type slugs | | `auth_config_ids` / `authConfigIds` | Array of auth config IDs | | `show_disabled` / `showDisabled` | Include disabled triggers (default: `false`) | ## Enable / Disable triggers [#enable--disable-triggers] Pause a trigger temporarily without deleting it: **Python:** ```python # Disable a trigger composio.triggers.disable(trigger_id="ti_abcd123") # Re-enable when needed composio.triggers.enable(trigger_id="ti_abcd123") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); // Disable a trigger await composio.triggers.disable('ti_abcd123'); // Re-enable when needed await composio.triggers.enable('ti_abcd123'); ``` ## Deleting triggers [#deleting-triggers] Permanently remove a trigger instance: **Python:** ```python composio.triggers.delete(trigger_id="ti_abcd123") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); await composio.triggers.delete('ti_abcd123'); ``` > Deleting a trigger is permanent. Use `disable()` instead to temporarily stop receiving events. --- # Custom OAuth webhooks (/docs/setting-up-triggers/custom-oauth-webhooks) This page is only for **realtime triggers where you bring your own OAuth app**. With Composio-managed OAuth, ingress is already set up and you can skip this entirely. Create the trigger and events flow. Some providers only deliver events to URLs you've registered on your OAuth app. When you bring your own app, you register Composio's ingress URL there once, so events can reach Composio. You need this only when the trigger type's `requires_webhook_endpoint_setup` flag is `true`. Each OAuth app you bring gets its own ingress URL within a project: ``` https://backend.composio.dev/api/v3.1/webhook_ingress/{toolkit_slug}/{we_xxx}/trigger_event ``` A single OAuth app can serve at most one Composio project: providers accept only one callback URL per OAuth app, and each ingress URL routes to a single project. In return, every project becomes its own webhook tenant, with: * **Its own ingress rate limit and backpressure budget** * **Project-scoped credentials**: the signing secret and app-level token you provide are stored against this project alone, never shared across projects. Repeat verification handshakes are rejected after the endpoint is verified, so the signing secret can't be silently swapped by a forged challenge. * **Clean fan-out**: events reach only that project's trigger instances * **Per-project metering** Every inbound event is signature-checked at ingress before any trigger fires: * **HMAC-SHA256** for Slack, **Ed25519** or shared-token matching for other providers * **Timestamp replay protection**: when the provider signs a request timestamp, requests outside the allowed skew window are rejected * **Unsigned or tampered requests** are rejected with `400` at ingress, so third parties can't spoof events onto your triggers > **Sharing one OAuth app across projects?** Consolidate to a single project or register separate OAuth apps per project before continuing. The walkthrough below uses Slack as the example and the [Webhook Endpoints API](/reference/api-reference/webhook-endpoints). For setup notes specific to each toolkit, see its FAQ section, for example [Slack](/toolkits/slack) or [Notion](/toolkits/notion). ## Step 1: Discover what credentials the endpoint needs [#step-1-discover-what-credentials-the-endpoint-needs] Call the schema endpoint for the toolkit. The `setup_fields` in the response tell you exactly what to collect from the provider's app dashboard. ```bash curl "https://backend.composio.dev/api/v3.1/webhook_endpoints/schema?toolkit_slug=slack" \ -H "x-api-key: " ``` Sample response: ```json { "toolkit_slug": "slack", "setup_fields": { "webhook_signing_secret": { "display_name": "Signing Secret", "description": "Webhook request signing secret from your Slack app dashboard", "is_required": true, "is_secret": true }, "app_token": { "display_name": "App-Level Token", "description": "Slack xapp- token with authorizations:read scope for event authorization", "is_required": true, "is_secret": true } } } ``` ## Step 2: Create the endpoint [#step-2-create-the-endpoint] ```bash curl -X POST "https://backend.composio.dev/api/v3.1/webhook_endpoints" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "toolkit_slug": "slack", "client_id": "" }' ``` Sample response: ```json { "id": "we_abc123", "toolkit_slug": "slack", "client_id": "", "webhook_url": "https://backend.composio.dev/api/v3.1/webhook_ingress/slack/we_abc123/trigger_event", "data": null, "created_at": "2026-04-24T10:00:00.000Z" } ``` Hold on to two values from the response: `id` (used as `` below) and `webhook_url` (you'll paste this into the provider's app dashboard in step 4). The call is **idempotent per `(toolkit_slug, client_id)` within a project**. Calling it again with the same pair returns the existing endpoint without rotating the URL or wiping the secret. ## Step 3: Store the credentials returned by the schema [#step-3-store-the-credentials-returned-by-the-schema] `PATCH` all the fields the schema returned in a single request. For Slack, that's the signing secret and (when needed) the app-level token together: ```bash curl -X PATCH "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "data": { "webhook_signing_secret": "", "app_token": "xapp-..." } }' ``` For Slack, the credentials come from: * **Signing secret**: Slack app → Basic Information → App Credentials → Signing Secret. * **App-level token**: Slack app → Basic Information → App-Level Tokens, with scope `authorizations:read`. Required for direct messages, private channels, and reactions. Omit it if you only need public-channel events. > **Store the credentials before you switch the provider's callback URL in step 4.** If the provider posts to the URL without a secret on the endpoint, every request fails with `400`, and the provider may auto-disable the endpoint after a window of consecutive failures (Slack: \~36 hours). ## Step 4: Point the provider's app dashboard at the URL [#step-4-point-the-providers-app-dashboard-at-the-url] Paste the `webhook_url` from step 2 into the provider's app dashboard: * **Slack** → Event Subscriptions → Request URL * **Notion** → Webhook Endpoints (in the integration's settings) For providers that issue a verification challenge on save (Slack `url_verification`, Notion's verification token, and so on), Composio responds automatically, with no handshake code on your side. Once the provider accepts the URL, go [create your trigger](/docs/setting-up-triggers/creating-triggers). ## Updating an endpoint [#updating-an-endpoint] To rotate the signing secret or update any single field, `PATCH` it (other fields are preserved): ```bash curl -X PATCH "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "data": { "webhook_signing_secret": "" } }' ``` To **replace** `data` wholesale (any field you don't include is cleared), `POST` to the same URL: ```bash curl -X POST "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "data": { "webhook_signing_secret": "", "app_token": "" } }' ``` The `webhook_url` is immutable for the lifetime of the endpoint. Rotating the signing secret on the provider side is a `PATCH` on the existing endpoint, not a new one. To inspect a single endpoint: ```bash curl "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \ -H "x-api-key: " ``` To list every endpoint in the current project: ```bash curl "https://backend.composio.dev/api/v3.1/webhook_endpoints" \ -H "x-api-key: " ``` ## Next [#next] - [Creating triggers](/docs/setting-up-triggers/creating-triggers): Activate a trigger for a user so events start flowing --- # Migration guides (/docs/migration-guide) > _Written December 2025._ ## Available migration guides [#available-migration-guides] **Migrating from Direct Tools to Sessions** Move from manual tool fetching and execution to the sessions paradigm. Your existing auth configs and connected accounts carry over. [View Direct Tools migration guide →](/docs/migration-guide/direct-to-sessions) **Migrating from MCP servers to Sessions** Move from per-toolkit MCP server configs (`composio.mcp.create` / `composio.mcp.generate`) to sessions. Your tools, auth configs, and connected accounts carry over — no re-auth. [View MCP servers migration guide →](/docs/migration-guide/mcp-servers-to-sessions) **Migrating from Experimental Tool Router** Migrate from `composio.experimental.tool_router` to the stable sessions API. [View Tool Router migration guide →](/docs/migration-guide/tool-router-beta) **Toolkit versioning migration** Migrate to use the toolkit versioning system. [View versioning migration guide →](/docs/migration-guide/toolkit-versioning) **New SDK migration** Migrate from the old Composio SDK to the new SDK, including breaking changes, new features, and updated APIs. [View SDK migration guide →](/docs/migration-guide/new-sdk) --- # Migrating from Direct Tools to Sessions (/docs/migration-guide/direct-to-sessions) > _Written February 2026._ This guide is for developers who are using Composio's **direct tool execution** pattern and want to migrate to **sessions**. For a comparison of the two patterns, see [Sessions vs Direct Execution](/docs/sessions-vs-direct-execution). With sessions, you don't need to manage tool fetching, authentication, or execution yourself. You create a session and it handles everything. Your existing auth configs and connected accounts carry over, so your users don't need to re-authenticate. If you're starting fresh, use [Configuring Sessions](/docs/configuring-sessions) instead. ## What changes [#what-changes] | | Direct tools | Sessions | | -------------------- | ------------------------------------------------- | ---------------------------------------------------------------------- | | **Tool discovery** | You fetch specific tools by name/toolkit | Agent discovers tools at runtime via meta tools | | **Authentication** | You manage connect links and wait for connections | In-chat auth prompts users automatically, or use `session.authorize()` | | **Execution** | You call `tools.execute()` with version pinning | Agent executes tools through `COMPOSIO_MULTI_EXECUTE_TOOL` | | **Toolkit versions** | You manage versions manually | Handled automatically | ## Migrating [#migrating] #### Pass your existing auth configs to the session You already have auth configs (e.g. `ac_github_config`, `ac_slack_config`). Pass them when creating a session so it uses your existing OAuth credentials and your users' connected accounts carry over. **Python:** ```python from composio import Composio composio = Composio() # Before: manual tool fetching with user_id # tools = composio.tools.get(user_id="user_123", toolkits=["GITHUB", "SLACK"]) # After: create a session with your existing auth configs session = composio.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", "slack": "ac_your_slack_config" } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Before: manual tool fetching with userId // const tools = await composio.tools.get("user_123", { toolkits: ["GITHUB", "SLACK"] }); // After: create a session with your existing auth configs const session = await composio.create("user_123", { authConfigs: { github: "ac_your_github_config", slack: "ac_your_slack_config", }, }); ``` Since the session uses the same `user_id` and auth configs, it automatically picks up existing connected accounts. No re-authentication needed. #### Replace manual tool fetching with session tools Instead of fetching specific tools by name, get the session's meta tools. These let the agent discover, authenticate, and execute any tool at runtime. **Python:** ```python # Before: manually specifying which tools to fetch # tools = composio.tools.get(user_id="user_123", toolkits=["GITHUB"]) # After: session provides meta tools that handle discovery tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); // Before: manually specifying which tools to fetch // const tools = await composio.tools.get("user_123", { toolkits: ["GITHUB"] }); // After: session provides meta tools that handle discovery const tools = await session.tools(); ``` #### Remove manual auth flows If you were manually creating connect links and waiting for connections, sessions handle this automatically. When a tool requires authentication, the agent prompts the user with a connect link in-chat. **Python:** ```python # Before: manual auth flow # connection_request = composio.connected_accounts.link( # user_id="user_123", # auth_config_id="ac_your_github_config", # callback_url="https://your-app.com/callback" # ) # redirect_url = connection_request.redirect_url # After: auth happens automatically in-chat # Or if you need to pre-authenticate outside of chat: connection_request = session.authorize("github") print(connection_request.redirect_url) connected_account = connection_request.wait_for_connection() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); // Before: manual auth flow // const connectionRequest = await composio.connectedAccounts.link("user_123", "ac_your_github_config", { // callbackUrl: "https://your-app.com/callback" // }); // const redirectUrl = connectionRequest.redirectUrl; // After: auth happens automatically in-chat // Or if you need to pre-authenticate outside of chat: const connectionRequest = await session.authorize("github", { callbackUrl: "https://your-app.com/callback", }); console.log(connectionRequest.redirectUrl); const connectedAccount = await connectionRequest.waitForConnection(); ``` #### Remove toolkit version management If you were pinning toolkit versions in your code or environment variables, you can remove that. Sessions handle versioning automatically. **Python:** ```python # Before: manual version pinning # composio = Composio( # toolkit_versions={ # "github": "20251027_00", # "slack": "20251027_00", # } # ) # After: no version management needed composio = Composio() session = composio.create(user_id="user_123") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; // Before: manual version pinning // const composio = new Composio({ // toolkitVersions: { // github: "20251027_00", // slack: "20251027_00", // }, // }); // After: no version management needed const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); ``` ## Restricting toolkits [#restricting-toolkits] If you were fetching tools from specific toolkits, you can restrict the session to only those toolkits: **Python:** ```python session = composio.create( user_id="user_123", toolkits=["github", "gmail", "slack"], auth_configs={ "github": "ac_your_github_config", "gmail": "ac_your_gmail_config", "slack": "ac_your_slack_config" } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { toolkits: ["github", "gmail", "slack"], authConfigs: { github: "ac_your_github_config", gmail: "ac_your_gmail_config", slack: "ac_your_slack_config", }, }); ``` ## Multiple connected accounts [#multiple-connected-accounts] If your users have multiple accounts for the same toolkit (e.g., work and personal Gmail), you can specify which one to use per session: **Python:** ```python session = composio.create( user_id="user_123", auth_configs={ "gmail": "ac_your_gmail_config" }, connected_accounts={ "gmail": ["ca_work_gmail"] } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { authConfigs: { gmail: "ac_your_gmail_config", }, connectedAccounts: { gmail: ["ca_work_gmail"], }, }); ``` If you don't specify, the most recently connected account is used. See [Managing multiple connected accounts](/docs/authentication/managing-multiple-connected-accounts) for details. ## Triggers [#triggers] Triggers don't work with sessions yet. Continue using them the same way you do today with `composio.triggers.create()`, `composio.triggers.enable()`, and webhook subscriptions. See [Triggers](/docs/triggers) for setup instructions. ## White labeling [#white-labeling] If you've set up white-labeled OAuth screens with custom auth configs, those carry over automatically. Just pass the same auth config IDs to your session via `auth_configs` / `authConfigs`. Your users will continue to see your branding on consent screens. See [White-labeling authentication](/docs/authentication/white-labeling-authentication) for more. ## Next [#next] - [Configuring Sessions](/docs/configuring-sessions): Toolkits, auth configs, account selection, and session methods --- # Migrating from MCP servers to Sessions (/docs/migration-guide/mcp-servers-to-sessions) > _Written June 2026._ This guide is for developers using Composio's **MCP servers** (`composio.mcp.create` / `composio.mcp.generate`, the "Single Toolkit MCP" flow) who want to migrate to **sessions**. Sessions are the next generation of the same idea: you still get an MCP URL that any MCP-compatible client connects to — but instead of standing up and managing a separate server config per toolkit, you create a session that handles tool discovery, authentication, context, and versioning for you. > Starting fresh? Skip this guide and read [Configuring Sessions](/docs/configuring-sessions). > Just connecting apps to your own agent for personal use — not building an app? You don't need the SDK. Use **Composio For You** to connect across 1000+ apps in a few clicks — switch to it from the product switcher in the top-left of the dashboard. Reserve `session.mcp.url` for programmatic, in-app use. ## What carries over (you keep all of this) [#what-carries-over-you-keep-all-of-this] * **Your tools** — every tool you exposed on a server is available in a session. * **Your auth configs and connected accounts** — pass the same `ac_…` IDs; your users **do not re-authenticate**. * **The MCP URL pattern** — you still get a URL (`session.mcp.url`) that plugs into your agent — any MCP-compatible client — the same way, the same protocol. * **Per-user isolation** — still keyed by `user_id`. * **Tool restriction** — you can still pin a session to an exact, fixed tool list (see Step 3). ## What changes [#what-changes] | | MCP servers (today) | Sessions | | --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | **Setup** | Create + manage a server config per toolkit (`composio.mcp.create`) | One `composio.create(user_id)` — no server object to manage | | **Get a URL** | `composio.mcp.generate(user_id, mcp_config_id)` → `instance.url` | `session.mcp.url` | | **Tools** | Fixed `allowed_tools` list, baked into the server | Dynamic discovery by default, **or** a fixed list (direct-tools preset) — your choice | | **Multiple toolkits** | One server per toolkit | One session spans many toolkits | | **Context** | All allowed tools always loaded | Managed — search/preload keep the agent's context lean | | **Versioning** | Manual | Handled automatically | | **Auth** | Pre-authenticate, then generate | Carries over; in-chat auth available, or `session.authorize()` | The *why*: a session is one managed endpoint that replaces N static server configs, gives the agent runtime tool discovery, and keeps context small — without losing the fixed-tool-list behavior you have today if that's what you want. ## Migrating [#migrating] #### Replace the server config + generate with a session The two-step "create a server, then generate a per-user URL" collapses into a single `composio.create(...)`. Pass the **same** toolkit, auth config, and tool list you had on the server. **Python:** ```python from composio import Composio composio = Composio(api_key="YOUR_API_KEY") # Before: create a server config, then generate a per-user URL # server = composio.mcp.create( # name="my-gmail-server", # toolkits=[{"toolkit": "gmail", "auth_config": "ac_xyz123"}], # allowed_tools=["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"], # ) # instance = composio.mcp.generate(user_id="user-123", mcp_config_id=server.id) # mcp_url = instance["url"] # After: one session, same toolkit + auth config + tools session = composio.create( user_id="user-123", toolkits=["gmail"], auth_configs={"gmail": "ac_xyz123"}, tools={"gmail": {"enable": ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"]}}, mcp=True, ) mcp_url = session.mcp.url ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); // Before: create a server config, then generate a per-user URL // const server = await composio.mcp.create("my-gmail-server", { // toolkits: [{ toolkit: "gmail", authConfigId: "ac_xyz123" }], // allowedTools: ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"], // }); // const instance = await composio.mcp.generate("user-123", server.id); // const mcpUrl = instance.url; // After: one session, same toolkit + auth config + tools const session = await composio.create("user-123", { toolkits: ["gmail"], authConfigs: { gmail: "ac_xyz123" }, tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"] } }, mcp: true, }); const { mcp } = session; const mcpUrl = mcp.url; ``` Same `user_id` + same auth config → existing connected accounts are picked up automatically. No re-auth. #### Point your MCP client at the new URL Swap the generated server URL for `session.mcp.url` wherever your agent connects. Nothing else about the client changes. ```python # Before: https://backend.composio.dev/v3/mcp/?user_id= # After: session.mcp.url ``` #### Keep an exact, fixed tool list (optional — closest 1:1 with your server) By default a session gives the agent **dynamic** tool discovery (meta-tools). If you want the *same static behavior* as your old server — a fixed set of tools, no discovery — add the **direct-tools preset**. This preloads exactly the tools you enable and turns meta-tools off. **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS session = composio.create( user_id="user-123", toolkits=["gmail"], auth_configs={"gmail": "ac_xyz123"}, tools={"gmail": {"enable": ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"]}}, session_preset=SESSION_PRESET_DIRECT_TOOLS, ) ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const session = await composio.create("user-123", { toolkits: ["gmail"], authConfigs: { gmail: "ac_xyz123" }, tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"] } }, sessionPreset: SessionPreset.DIRECT_TOOLS, }); ``` Leave the preset off to get the upgrade: the agent discovers and loads tools at runtime, so you can span many toolkits without bloating context. ## Beyond the MCP URL [#beyond-the-mcp-url] A session isn't only an MCP endpoint — it's also a normal SDK object, which is handy for non-agent code paths. ### Native tools for your framework [#native-tools-for-your-framework] `session.tools()` returns provider-wrapped native tools for OpenAI, Anthropic, LangChain, the Vercel AI SDK, and others, so you skip manual schema wiring. With the direct-tools preset it returns your exact fixed tool set; by default it returns the meta-tools that drive runtime discovery. **Python:** ```python tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const session = await composio.create("user-123"); const tools = await session.tools(); ``` ### Execute a tool without an LLM [#execute-a-tool-without-an-llm] For a deterministic, non-agent path, call a tool directly on the session. **Python:** ```python result = session.execute( "GITHUB_CREATE_ISSUE", arguments={"owner": "my-org", "repo": "my-repo", "title": "Fix login bug"}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const session = await composio.create("user-123"); const result = await session.execute("GITHUB_CREATE_ISSUE", { owner: "my-org", repo: "my-repo", title: "Fix login bug", }); ``` ## Things to know [#things-to-know] * **Multiple toolkits on one endpoint** — where you ran several single-toolkit servers, one session spans them all (`toolkits=["gmail", "slack", "github"]`). Fewer moving parts. * **Reuse one session — don't create one per request** — sessions persist and are reusable. Create a session once and keep using it across calls; see [Configuring Sessions](/docs/configuring-sessions). * **Sharing one account across users** — sessions support **shared connections** (`account_type:"SHARED"` + an allow/deny ACL), pinned per session. See [Shared connections](/docs/extending-sessions/shared-connections). * **Tenant-specific params** (SharePoint sub-site, Jira subdomain) — prefill them via shared credentials on the auth config. * **White-labeling carries over** — pass the same white-labeled auth config IDs; users keep seeing your branding on consent screens. See [White-labeling authentication](/docs/authentication/white-labeling-authentication). * **Triggers** — unchanged; continue using `composio.triggers.*` and webhooks (triggers aren't part of sessions yet). * **Dashboard** — sessions are created via the SDK. For no-code, personal app connections, use **Composio For You** — reachable from the product switcher in the top-left of the dashboard. ## Next [#next] - [Configuring Sessions](/docs/configuring-sessions): Toolkits, auth configs, account selection, presets, and session methods --- # Migrating from Experimental Tool Router (/docs/migration-guide/tool-router-beta) > **Legacy · written January 2026.** This is a point-in-time migration/legacy guide and may describe outdated APIs. For current guidance, see https://docs.composio.dev. This guide is for users who adopted the **experimental tool router** (`composio.experimental.tool_router`) during its beta period. The tool router has now graduated to a stable, first-class feature called **sessions** — with a simpler API, better auth handling, and full framework support. If you never used `composio.experimental.tool_router`, you can skip this guide and start with [Configuring Sessions](/docs/configuring-sessions). ## The basics [#the-basics] #### Upgrade composio package Upgrade to the latest stable version: **Python:** ```bash pip install --upgrade composio ``` **TypeScript:** #### Update session creation **Python:** ```python # Beta (before) session = composio.experimental.tool_router.create_session( user_id="user@example.com" ) # Stable (after) session = composio.create( user_id="user@example.com" ) ``` **TypeScript:** ```typescript // Beta (before) const session = await composio.experimental.toolRouter.createSession('user_123'); // Stable (after) const session = await composio.create('user_123'); ``` #### Moving users (optional) If you have existing users on tool router and you don't want them to authenticate again: * Tool Router will auto-detect auth configs and connected accounts it created (from the beta version). * If you have custom auth configs (not created by Tool Router): * Search for the Auth config for that connected account. See [Connected Accounts](/docs/auth-configuration/connected-accounts) to fetch existing accounts programmatically. * While creating a session configure to use this Auth config. * You need to repeat this for each toolkit you want to enable for that session. **Python:** ```python session = composio.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", "slack": "ac_your_slack_config" } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create('user_123', { authConfigs: { github: "ac_your_github_config", slack: "ac_your_slack_config", }, } ); ``` ## Next [#next] - [Configuring Sessions](/docs/configuring-sessions): Restrict toolkits, set auth configs, and select connected accounts --- # Toolkit versioning migration (/docs/migration-guide/toolkit-versioning) > _Written October 2025._ Starting with Python SDK v0.9.0 and TypeScript SDK v0.2.0, manual tool execution requires explicit version specification. This is a breaking change from earlier versions where toolkit versioning was optional. ## Breaking change [#breaking-change] Manual tool execution now requires explicit version specification. The `tools.execute()` method will fail without a version. ### Before (will fail) [#before-will-fail] **Python:** ```python # Raises ToolVersionRequiredError result = composio.tools.execute( "GITHUB_CREATE_ISSUE", {"user_id": "user-123", "arguments": {...}} ) ``` **TypeScript:** ```typescript // Throws ComposioToolVersionRequiredError const result = await composio.tools.execute({ toolSlug: "GITHUB_CREATE_ISSUE", userId: "user-123", arguments: {...} }); ``` ### After (required) [#after-required] Choose one of three migration strategies: #### Option 1: Configure version at SDK level [#option-1-configure-version-at-sdk-level] **Python:** ```python from composio import Composio # Pin specific versions for each toolkit composio = Composio( api_key="YOUR_API_KEY", toolkit_versions={ "github": "20251027_00", "slack": "20251027_00", "gmail": "20251027_00" } ) ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; // Pin specific versions for each toolkit const composio = new Composio({ apiKey: "YOUR_API_KEY", toolkitVersions: { github: "20251027_00", slack: "20251027_00", gmail: "20251027_00" } }); ``` #### Option 2: Pass version with each execution [#option-2-pass-version-with-each-execution] **Python:** ```python # Specify version directly in execute call result = composio.tools.execute( "GITHUB_LIST_STARGAZERS", arguments={ "owner": "ComposioHQ", "repo": "composio" }, user_id="user-k7334", version="20251027_00" # Override version for this execution ) print(result) ``` **TypeScript:** ```typescript // Specify version directly in execute call const result = await composio.tools.execute("GITHUB_LIST_STARGAZERS", { userId: "user-k7334", arguments: { owner: "ComposioHQ", repo: "composio" }, version: "20251027_00" // Override version for this execution }); console.log(result); ``` #### Option 3: Use environment variables [#option-3-use-environment-variables] ```bash export COMPOSIO_TOOLKIT_VERSION_GITHUB="20251027_00" ``` ## Migration checklist [#migration-checklist] 1. **Audit your code**: Find all `tools.execute()` calls in your codebase 2. **Choose a strategy**: Select one of the three options above based on your needs 3. **Test thoroughly**: Verify tools work correctly with pinned versions 4. **Deploy gradually**: Roll out changes incrementally to minimize risk ## Temporary workaround [#temporary-workaround] During migration, you can temporarily skip version checks (not recommended for production): **Python:** ```python result = composio.tools.execute( "GITHUB_CREATE_ISSUE", { "user_id": "user-123", "arguments": {...} }, dangerously_skip_version_check=True ) ``` **TypeScript:** ```typescript const result = await composio.tools.execute({ toolSlug: "GITHUB_CREATE_ISSUE", userId: "user-123", arguments: {...}, dangerouslySkipVersionCheck: true }); ``` > The `dangerouslySkipVersionCheck` flag is only for migration or debugging. Never use in production. ## Next [#next] - [Migrate to sessions](/docs/migration-guide/direct-to-sessions): Move from older tool execution patterns to sessions --- # Our next generation SDKs (/docs/migration-guide/new-sdk) > **Legacy · written December 2025.** This is a point-in-time migration/legacy guide and may describe outdated APIs. For current guidance, see https://docs.composio.dev. > This guide covers migrating from the legacy SDK (v1) — `composio-core` on PyPI and npm — to the current SDK (v3): `composio` on PyPI, `@composio/core` on npm. Provider packages such as `composio-openai` kept their names across the rewrite: current releases work with v3, v1-era releases don't. The recommended way to use Composio is now through **sessions** — see [Migrating from Direct Tools to Sessions](/docs/migration-guide/direct-to-sessions) or [Configuring Sessions](/docs/configuring-sessions) if you're starting fresh. In the last few months, we have experienced very rapid growth in usage of our platform. As such, our team has been working hard to radically improve the performance and developer experience of our platform. A lot of these changes have happened in the background, but we are excited to finally share our new SDKs with you that complement our new infra. The new API features improved usability, enhanced stability, and better scalability. The SDKs built on top of it simplify the developer experience, making it easier than ever to build useful agents. ## What's new? [#whats-new] A lot of the changes are on the infra side, but from the SDK point of view, here is what you can expect: * Faster and more reliable tool execution * A simpler but more opinionated SDK * Much more intuitive and consistent naming conventions * A vastly improved TypeScript SDK that is meaningfully more type-safe and has full feature parity with the Python SDK There aren't too many new flashy features here (yet) mainly because we wanted to get the bones right — but we feel we have a solid foundation to ship incredible new experiences on top very quickly. ## State of the new SDK and what is happening with the old SDKs? [#state-of-the-new-sdk-and-what-is-happening-with-the-old-sdks] Currently, the new SDKs are in a preview release. These new SDKs come almost fully formed, we do not expect many breaking changes to them but are releasing them in a preview state to get feedback and make necessary changes before locking them in. As we lock the new SDKs in place, we will deprecate support for the old SDKs. They will continue to work for the foreseeable future but are no longer actively maintained. We will continue to push security updates and fix any critical bugs but will not support any new functionality in them. We urge you to upgrade to the new SDKs as soon as possible. ## Nomenclature [#nomenclature] We have updated several key terms in the SDK and API to improve clarity and consistency. The following table summarizes these changes: | Previous Term | Current Term | Definition | | ------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | Actions | Tools | Individual operations or capabilities that can be performed by an LLM agent | | Apps | Toolkits | A collection of tools grouped under a single application | | Integration | Auth Config | Configuration containing developer credentials and application-level settings such as scopes and API endpoints. Scoped to a toolkit. | | Connection | Connected accounts | User-linked accounts associated with a toolkit | | Entity ID | userID | The identifier of the user performing the action (UUID or email) | | Trigger | Trigger | An event that can be subscribed to | | Toolsets | Providers | LLM or agent framework that can be used with Composio to create agents | ## Switch to nano IDs from UUIDs [#switch-to-nano-ids-from-uuids] We have transitioned from UUIDs to nano IDs throughout the platform for the following reasons: * **Improved readability**: UUIDs are lengthy and difficult to read * **Better usability**: Easier to copy with a single double-click * **Better organization**: Nano IDs allow us to distinguish between different resource types through prefixes | Feature | Nano ID Prefix | Example | | ----------------- | -------------- | ----------------- | | Connected Account | `ca_` | `ca_8x9w2l3k5m` | | Auth Config | `ac_` | `ac_1234567890` | | Trigger | `ti_` | `ti_So9EQf8XnAcy` | > Nano IDs are short, unique, and prefixed to indicate the resource type. ## SDK Changes [#sdk-changes] Upgrade to the latest SDK version using the appropriate package manager: **Python:** ```bash pip install -U composio ``` **TypeScript:** Both SDKs now implement proper namespacing for each concept. ### UserID scoping [#userid-scoping] The concept of `entity_id` has been expanded and renamed to `user_id`. All operations are now scoped to a userID, including: * Fetching tools * Initiating connections * Executing tools * Managing triggers This change provides explicit specification of the user for whom the action is being performed. When a user may have multiple accounts (such as work and personal Gmail connections), you can use the more specific connected account ID. ### Replacing ToolSets with Providers [#replacing-toolsets-with-providers] We have deprecated "toolsets" in favor of "providers". This change allows Composio to provide deeper standardization for tool implementation across different frameworks. Previously, you needed to import and use a framework-specific `ComposioToolSet` class: **Python (previous):** ```python from composio_openai import ComposioToolSet, Action, App from openai import OpenAI toolset = ComposioToolSet() ``` **TypeScript (previous):** ```typescript import { OpenAIToolSet } from 'composio-core'; const toolset = new OpenAIToolSet(); ``` The SDK structure is now framework-agnostic and includes the OpenAI provider out of the box: **Python (current):** ```python from composio import Composio # from composio_langchain import LangchainProvider composio = Composio() # composio = Composio(provider=LangchainProvider()) tools = composio.tools.get( user_id="0001", tools=["LINEAR_CREATE_LINEAR_ISSUE", "GITHUB_CREATE_COMMIT"] ) # tools returned is formatted for the provider. by default, OpenAI. ``` **TypeScript (current):** ```typescript import { Composio } from '@composio/core'; // import { VercelProvider } from '@composio/vercel'; const composio = new Composio({ // provider: new VercelProvider(), }); // Can specify other providers too, like OpenAI, Anthropic, Vercel AI SDK. const tools = await composio.tools.get('user@example.com', { tools: ['LINEAR_CREATE_LINEAR_ISSUE', 'GITHUB_CREATE_COMMIT'], }); // tools returned is formatted for the provider. by default, OpenAI. ``` You can now use the same tools across any framework with our unified interface, or create custom toolsets for frameworks we don't yet support. Read more about [providers in our documentation](/docs/providers/openai) and explore the [complete list of available providers](/docs/providers/openai). ### Fetching and filtering tools [#fetching-and-filtering-tools] Previously, you could filter tools by: * Apps * Action names (tool names) * Tags You could also specify an `important` flag to retrieve the most important tools: **Python (previous):** ```python from composio_openai import ComposioToolSet, Action, App from openai import OpenAI toolset = ComposioToolSet() client = OpenAI() tools = toolset.get_tools( actions=[Action.GITHUB_GET_THE_AUTHENTICATED_USER], check_connected_accounts=True ) tools = toolset.get_tools(apps=[App.GITHUB, App.LINEAR, App.SLACK], check_connected_accounts=True) ``` **TypeScript (previous):** ```typescript import { OpenAIToolSet } from 'composio-core'; const toolset = new OpenAIToolSet(); const tools_1 = await toolset.getTools({ apps: ['GITHUB'] }); const tools_2 = await toolset.getTools({ actions: ['GITHUB_GET_THE_AUTHENTICATED_USER', 'LINEAR_CREATE_LINEAR_ISSUE'], }); ``` You can now filter tools by: * Toolkits * Tool slugs * Limit parameter * Search query The `important` flag has been removed. Instead, tools are returned in order of importance by default: > Since `user_id` is now explicitly required, the `check_connected_accounts` flag is no longer necessary. **Python (current):** ```python from composio import Composio composio = Composio() user_id = "user@acme.org" tools_1 = composio.tools.get(user_id=user_id, toolkits=["GITHUB", "LINEAR"]) tools_2 = composio.tools.get(user_id=user_id, toolkits=["SLACK"], limit=5) # Default limit=20 tools_3 = composio.tools.get( user_id=user_id, tools=["GITHUB_CREATE_AN_ISSUE", "GITHUB_CREATE_AN_ISSUE_COMMENT", "GITHUB_CREATE_A_COMMIT"], ) tools_4 = composio.tools.get(user_id="john", search="hackernews posts") ``` **TypeScript (current):** ```typescript import { Composio } from '@composio/core'; const userId = 'user@acme.org'; const composio = new Composio(); const tools_1 = await composio.tools.get(userId, { toolkits: ['GITHUB', 'LINEAR'], }); const tools_2 = await composio.tools.get(userId, { toolkits: ['GITHUB'], limit: 5, // Default limit=20 }); const tools_3 = await composio.tools.get(userId, { tools: ['GITHUB_CREATE_AN_ISSUE', 'GITHUB_CREATE_AN_ISSUE_COMMENT', 'GITHUB_CREATE_A_COMMIT'], }); const tools_4 = await composio.tools.get(userId, { search: 'hackernews posts', }); ``` ### Fetching raw tool data [#fetching-raw-tool-data] To examine the raw schema definition of a tool for understanding input/output parameters or building custom logic around tool definitions, use the following methods: **Python (current):** ```python from composio import Composio composio = Composio() tool = composio.tools.get_raw_composio_tool_by_slug("HACKERNEWS_GET_LATEST_POSTS") print(tool.model_dump_json()) ``` **TypeScript (current):** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const tool = await composio.tools.getRawComposioToolBySlug('GITHUB_GET_OCTOCAT'); console.log(JSON.stringify(tool, null, 2)); ``` ### Executing tools [#executing-tools] Tool execution remains largely unchanged, with `user_id` now explicitly required. For agentic frameworks, the tool object returned from `tools.get` is now the respective framework's native tool object. Tool call execution is handled by the agentic framework itself. > For non-agentic frameworks, Composio provides a helper function to execute tool calls. **Python v3:** ```python from composio import Composio from openai import OpenAI openai_client = OpenAI() composio = Composio() tools = composio.tools.get(user_id="user@acme.com", tools=["GITHUB_GET_THE_ZEN_OF_GITHUB"]) response = openai_client.chat.completions.create( model="gpt-4.1", messages=[{"role": "user", "content": "gimme some zen."}], tools=tools, ) result = composio.provider.handle_tool_calls(user_id="user@acme.com", response=response) print(result) ``` **TypeScript v3:** ```typescript import { Composio } from '@composio/core'; import { AnthropicProvider } from '@composio/anthropic'; import Anthropic from '@anthropic-ai/sdk'; const anthropic = new Anthropic(); const composio = new Composio({ provider: new AnthropicProvider(), }); const userId = 'user@example.com'; const tools = await composio.tools.get(userId, { toolkits: ['GMAIL'], }); const msg = await anthropic.messages.create({ model: 'claude-3-7-sonnet-latest', tools: tools, messages: [ { role: 'user', content: "Say hi to 'soham@composio.dev'", }, ], max_tokens: 1024, }); const result = await composio.provider.handleToolCalls(userId, msg); console.log('Tool results:', result); ``` For more information on executing tools for different frameworks, see [Replacing ToolSets with Providers](#replacing-toolsets-with-providers). ### Tool Modifiers (formerly Tool Processors) [#tool-modifiers-formerly-tool-processors] Tool processors have been renamed to *tool modifiers* and now provide an improved developer experience. The implementation is now available in TypeScript too! (previously Python-only). ```python title="Python (previous)" from composio_openai import ComposioToolSet, Action toolset = ComposioToolSet() def my_schema_processor(schema: dict) -> dict: ... def my_preprocessor(inputs: dict) -> dict: ... def my_postprocessor(result: dict) -> dict: ... # Get tools with the modified schema processed_tools = toolset.get_tools( actions=[Action.GMAIL_SEND_EMAIL], processors={ # Applied BEFORE the LLM sees the schema "schema": {Action.SOME_ACTION: my_schema_processor}, # Applied BEFORE the tool executes "pre": {Action.SOME_ACTION: my_preprocessor}, # Applied AFTER the tool executes, BEFORE the result is returned "post": {Action.SOME_ACTION: my_postprocessor}, }, ) ``` | Previous | Current | | ------------------ | ------------------------ | | `pre` processor | `beforeExecute` modifier | | `post` processor | `afterExecute` modifier | | `schema` processor | `schema` modifier | The modifiers now leverage language-specific features to provide a more natural developer experience. While tool processors could previously be applied during SDK initialization, tool fetching, and tool execution, we have restructured them as follows: * **Chat Completion providers**: Modifiers are specified and applied during tool execution * **Agentic frameworks**: Modifiers are specified and applied during tool fetching #### Schema Modifiers [#schema-modifiers] The following example demonstrates schema modifier usage, applicable across all providers: **Python (current):** ```python from composio import Composio, schema_modifier from composio.types import Tool user_id = "your@email.com" @schema_modifier(tools=["HACKERNEWS_GET_LATEST_POSTS"]) def modify_schema( tool: str, toolkit: str, schema: Tool, ) -> Tool: _ = schema.input_parameters["properties"].pop("page", None) schema.input_parameters["required"] = ["size"] return schema tools = composio.tools.get( user_id=user_id, tools=["HACKERNEWS_GET_LATEST_POSTS", "HACKERNEWS_GET_USER"], modifiers=[ modify_schema, ] ) ``` **TypeScript (current):** ```typescript // @noErrors import { Composio } from '@composio/core'; import { OpenAI } from 'openai'; const userId = 'your@email.com'; const composio = new Composio(); // Schema modifier to delete the `page` argument from the `HACKERNEWS_GET_LATEST_POSTS` tool const tools = await composio.tools.get( userId, { tools: ['HACKERNEWS_GET_LATEST_POSTS', 'HACKERNEWS_GET_USER'], }, { modifySchema: ({ toolSlug, toolkitSlug, schema }) => { if (toolSlug === 'HACKERNEWS_GET_LATEST_POSTS') { const { inputParameters } = schema; if (inputParameters?.properties) { delete inputParameters.properties['page']; } inputParameters.required = ['size']; } return schema; }, } ); console.log(JSON.stringify(tools, null, 2)); ``` #### Before Modifiers [#before-modifiers] The following example shows creating and using a before modifier for a Chat Completion provider. For agentic frameworks, view the [complete before modifier documentation](/docs/tools-direct/modify-tool-behavior/before-execution-modifiers): **Python (current):** ```python @before_execute(tools=["HACKERNEWS_GET_LATEST_POSTS"]) def before_execute_modifier( tool: str, toolkit: str, params: ToolExecuteParams, ) -> ToolExecuteParams: params["arguments"]["size"] = 1 return params # Get tools tools = composio.tools.get(user_id=user_id, slug="HACKERNEWS_GET_LATEST_POSTS") ``` **TypeScript (current):** ```typescript // @noErrors const result_1 = await composio.tools.execute( 'HACKERNEWS_GET_LATEST_POSTS', { userId, arguments: JSON.parse(toolArgs), }, { beforeExecute: ({ toolSlug, toolkitSlug, params }) => { if (toolSlug === 'HACKERNEWS_GET_LATEST_POSTS') { params.arguments.size = 1; } console.log(params); return params; }, } ); ``` #### After Modifiers [#after-modifiers] The following example shows creating and using an after modifier for a Chat Completion provider. For agentic frameworks, view the [complete after modifier documentation](/docs/tools-direct/modify-tool-behavior/after-execution-modifiers): **Python (current):** ```python @after_execute(tools=["HACKERNEWS_GET_USER"]) def after_execute_modifier( tool: str, toolkit: str, response: ToolExecutionResponse, ) -> ToolExecutionResponse: return { **response, "data": { "karma": response["data"]["karma"], }, } tools = composio.tools.get(user_id=user_id, slug="HACKERNEWS_GET_USER") ``` **TypeScript (current):** ```typescript // @noErrors const result_2 = await composio.tools.execute( 'HACKERNEWS_GET_USER', { userId, arguments: JSON.parse(toolArgs), }, { afterExecute: ({ toolSlug, toolkitSlug, result }) => { if (toolSlug === 'HACKERNEWS_GET_USER') { const { data } = result; const { karma } = data.response_data as { karma: number }; return { ...result, data: { karma }, }; } return result; }, } ); ``` ### Custom Tools [#custom-tools] Custom tools are now session-scoped. Define local tools with the experimental custom-tools API and attach them when creating or reusing a session. **Python:** ```python from composio import Composio from pydantic import BaseModel, Field composio = Composio() class GetIssueInfoInput(BaseModel): issue_number: int = Field(..., description="The issue number") @composio.experimental.tool(extends_toolkit="github") def get_issue_info(input: GetIssueInfoInput, ctx) -> dict: """Get information about a GitHub issue.""" result = ctx.proxy_execute( toolkit="github", endpoint=f"/repos/composiohq/composio/issues/{input.issue_number}", method="GET", ) return {"data": result["data"]} session = composio.create( user_id="default", experimental={"custom_tools": [get_issue_info]}, ) ``` **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; const composio = new Composio(); const getIssueInfo = experimental_createTool("GET_ISSUE_INFO", { name: "Get issue info", description: "Get information about a GitHub issue.", extendsToolkit: "github", inputParams: z.object({ issueNumber: z.number().describe("The issue number"), }), execute: async (input, ctx) => { const result = await ctx.proxyExecute({ toolkit: "github", endpoint: `/repos/composiohq/composio/issues/${input.issueNumber}`, method: "GET", }); return { data: result.data }; }, }); const session = await composio.create("default", { experimental: { customTools: [getIssueInfo] }, }); ``` For more information, see [Custom Tools and Toolkits](/docs/extending-sessions/custom-tools-and-toolkits). ### Auth configs (formerly integrations) [#auth-configs-formerly-integrations] Integrations are now called *auth configs*. While the terminology has changed, the underlying concept remains the same. Auth configs store the configuration required for authentication with a given toolkit, including OAuth developer credentials, configurable base URLs, and scopes. Auth configs now use nano IDs instead of UUIDs: | Previous (UUID) Example | Current (Nano ID) Example | | :------------------------------------- | :------------------------ | | `b7a9c1e2-3f4d-4a6b-8c2e-1d2f3a4b5c6d` | `ac_8x9w2l3k5m` | We recommend storing auth config nano IDs in your database for connecting users to the appropriate auth configuration. For most use cases, you will create auth configs through the dashboard, and this process remains unchanged. Read more about [creating auth configs](/docs/tools-direct/authenticating-tools#creating-an-auth-config) and [customizing auth configs](/docs/auth-configuration/custom-auth-configs). Creating auth configs programmatically in the previous SDK: **Python (previous):** ```python from composio_openai import App, ComposioToolSet toolset = ComposioToolSet() integration = toolset.create_integration( app=App.GITHUB, auth_mode="OAUTH2", use_composio_oauth_app=True, # For use_composio_oauth_app=False, you can provide your own OAuth app credentials here # auth_config={ # "client_id": "123456", # "client_secret": "123456" # } ) print(integration.id) ``` **TypeScript (previous):** ```typescript // @noErrors import { OpenAIToolSet } from "composio-core"; const composioToolset = new OpenAIToolSet(); const integration = await composioToolset.integrations.create({ name: "gmail_integration", appUniqueKey: "gmail", forceNewIntegration: true, useComposioAuth: false, // For useComposioAuth: false, you can provide your own OAuth app credentials here // authScheme: "OAUTH2", // authConfig: { // clientId: "123456", // clientSecret: "123456" // } }) console.log(integration.id) ``` Creating auth configs programmatically in the current SDK: **Python (current):** ```python from composio import Composio composio = Composio() # Use composio managed auth auth_config = composio.auth_configs.create( toolkit="notion", options={ "type": "use_composio_managed_auth", # "type": "use_custom_auth", # "auth_scheme": "OAUTH2", # "credentials": { # "client_id": "1234567890", # "client_secret": "1234567890", # "oauth_redirect_uri": "https://backend.composio.dev/api/v3/toolkits/auth/callback", # }, }, ) print(auth_config) ``` **TypeScript (current):** ```typescript // @noErrors import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('LINEAR', { name: 'Linear', type: 'use_composio_managed_auth', // type: "use_custom_auth", // credentials: { // client_id: "1234567890", // client_secret: "1234567890", // oauth_redirect_uri: "https://backend.composio.dev/api/v3/toolkits/auth/callback", // }, }); console.log(authConfig); ``` For using custom authentication credentials, refer to the [Programmatic Auth Configs](/docs/authentication/programmatic-auth-configs) documentation. > The callback URL for creating custom OAuth configs is now `https://backend.composio.dev/api/v3/toolkits/auth/callback`. The previous URL was `https://backend.composio.dev/api/v1/auth-apps/add`. ### Connected accounts / User IDs [#connected-accounts--user-ids] The primary change in connected accounts and user IDs is that user IDs are now a more prominent concept compared to entities in previous versions. We have simplified the process of connecting a user to a toolkit. Instead of multiple methods and parameters for initiating a connection, both the SDK and API now require only a `user_id` and `auth_config_id` to initiate a connection. This approach is more explicit and works well with the ability for developers to have multiple auth configs for a given toolkit. Connected accounts now use nano IDs instead of UUIDs: | Previous (UUID) Example | Current (Nano ID) Example | | :------------------------------------- | :------------------------ | | `b7a9c1e2-3f4d-4a6b-8c2e-1d2f3a4b5c6d` | `ca_8x9w2l3k5m` | Previously, you might have initiated a connection like this: **Python (previous):** ```python from composio_openai import ComposioToolSet toolset = ComposioToolSet() user_id = "your_user_unique_id" google_integration_id = "0000-0000" entity = toolset.get_entity(id=user_id) try: print(f"Initiating OAuth connection for entity {entity.id}...") connection_request = toolset.initiate_connection( integration_id=google_integration_id, entity_id=user_id, # Optionally add: redirect_url="https://yourapp.com/final-destination" # if you want user sent somewhere specific *after* Composio finishes. ) # Check if a redirect URL was provided (expected for OAuth) if connection_request.redirectUrl: print(f"Received redirect URL: {connection_request.redirectUrl}") else: print("Error: Expected a redirectUrl for OAuth flow but didn't receive one.") except Exception as e: print(f"Error initiating connection: {e}") ``` **TypeScript (previous):** ```typescript // @noErrors import { OpenAIToolSet } from "composio-core"; const toolset = new OpenAIToolSet(); const userId = "your_user_unique_id"; const googleIntegrationId = "0000-0000"; console.log(`Initiating OAuth connection for entity ${userId}...`); const connectionRequest = await toolset.connectedAccounts.initiate({ integrationId: googleIntegrationId, entityId: userId, // Optionally add: redirectUri: "https://yourapp.com/final-destination" // if you want user sent somewhere specific *after* Composio finishes. }); // Check if a redirect URL was provided (expected for OAuth) if (connectionRequest?.redirectUrl) { console.log(`Received redirect URL: ${connectionRequest.redirectUrl}`); // Proceed to Step 2: Redirect the user // Return or pass connectionRequest to the next stage } else { console.error("Error: Expected a redirectUrl for OAuth flow but didn't receive one."); } ``` The current process for initiating a connection is as follows: **Python (current):** ```python from composio import Composio linear_auth_config_id = "ac_1234" user_id = "user@email.com" composio = Composio() # Create a new connected account connection_request = composio.connected_accounts.initiate( user_id=user_id, auth_config_id=linear_auth_config_id, ) print(connection_request.redirect_url) # Wait for the connection to be established connected_account = connection_request.wait_for_connection() print(connected_account) ``` **TypeScript (current):** ```typescript // @noErrors import { Composio } from "@composio/core"; const composio = new Composio(); const linearAuthConfigId = "ac_1234"; const userId = "user@email.com"; // Initiate the OAuth connection request const connRequest = await composio.connectedAccounts.initiate(userId, linearAuthConfigId); const { redirectUrl, id } = connRequest; console.log(redirectUrl); // Wait for the connection to be established await connRequest.waitForConnection(); // If you only have the connection request ID, you can also wait using: await composio.connectedAccounts.waitForConnection(id); ``` ### Triggers [#triggers] Composio continues to support listening to application events using triggers through WebSockets and webhooks. #### Creating triggers [#creating-triggers] The process for creating triggers and specifying their configuration has been redesigned for improved clarity and intuitiveness. Some triggers require configuration, such as repository names for GitHub triggers or channel names for Slack triggers. The process usually follows the pattern of fetching the trigger type and then creating the trigger with the appropriate configuration. **Python (current):** ```python from composio import Composio composio = Composio() user_id = "user@example.com" trigger_config = composio.triggers.get_type("GITHUB_COMMIT_EVENT") print(trigger_config.config) ### Trigger Config # { # "properties": { # "owner": { # "description": "Owner of the repository", # "title": "Owner", # "type": "string" # }, # "repo": { # "description": "Repository name", # "title": "Repo", # "type": "string" # } # }, # "required": ["owner", "repo"], # "title": "WebhookConfigSchema", # "type": "object" trigger = composio.triggers.create( slug="GITHUB_COMMIT_EVENT", user_id=user_id, trigger_config={"repo": "composiohq", "owner": "composio"}, ) print(trigger) # Managing triggers composio.triggers.enable(id="ti_abcd123") ``` **TypeScript (current):** ```typescript // @noErrors import { Composio } from '@composio/core'; const composio = new Composio(); const userId = 'user@acme.com'; // Fetch the trigger details const triggerType = await composio.triggers.getType('GITHUB_COMMIT_EVENT'); console.log(JSON.stringify(triggerType.config, null, 2)); /*--- Trigger config --- { "properties": { "owner": { "description": "Owner of the repository", "title": "Owner", "type": "string" }, "repo": { "description": "Repository name", "title": "Repo", "type": "string" } }, "required": ["owner", "repo"], "title": "WebhookConfigSchema", "type": "object" } */ const createResponse = await composio.triggers.create(userId, 'GITHUB_COMMIT_EVENT', { triggerConfig: { owner: 'composiohq', repo: 'composio', }, }); console.log(createResponse); ``` #### Enabling/Disabling triggers [#enablingdisabling-triggers] You can enable or disable triggers through either the SDK or the dashboard. The dashboard process remains unchanged. Managing triggers with the SDK: **Python:** ```python # Disable a trigger instance disabled_instance = composio.triggers.disable(trigger_id="ti_abcd123") print(disabled_instance) ``` **TypeScript:** ```typescript // @noErrors await composio.triggers.disable("ti_abcd123"); ``` If needed, the trigger can be enabled again. **Python:** ```python # Enable a trigger instance enabled_instance = composio.triggers.enable(trigger_id="ti_abcd123") print(enabled_instance) ``` **TypeScript:** ```typescript // @noErrors await composio.triggers.enable("ti_abcd123"); ``` #### Listening to triggers [#listening-to-triggers] We recommend listening to triggers through webhooks. The following are example routes for Next.js and FastAPI. For development, you can also [listen to triggers through the SDK](/docs/setting-up-triggers/subscribing-to-events#receive-events-locally). **FastAPI:** ```python title="app/route.py" from fastapi import FastAPI, Request, HTTPException from typing import Dict, Any import uvicorn import json import hmac import hashlib import base64 import os def verify_webhook_signature(request: Request, body: bytes) -> bool: """Verify Composio webhook signature""" webhook_signature = request.headers.get("webhook-signature") webhook_id = request.headers.get("webhook-id") webhook_timestamp = request.headers.get("webhook-timestamp") webhook_secret = os.getenv("COMPOSIO_WEBHOOK_SECRET") if not all([webhook_signature, webhook_id, webhook_timestamp, webhook_secret]): raise HTTPException(status_code=400, detail="Missing required webhook headers or secret") if not webhook_signature.startswith("v1,"): raise HTTPException(status_code=401, detail="Invalid signature format") received = webhook_signature[3:] signing_string = f"{webhook_id}.{webhook_timestamp}.{body.decode()}" expected = base64.b64encode( hmac.new(webhook_secret.encode(), signing_string.encode(), hashlib.sha256).digest() ).decode() if not hmac.compare_digest(received, expected): raise HTTPException(status_code=401, detail="Invalid webhook signature") return True @app.post("/webhook") async def webhook_handler(request: Request): payload = await request.json() trigger_type = payload.get("type") event_data = payload.get("data", {}) if trigger_type == "github_star_added_event": repo_name = event_data.get("repository_name") starred_by = event_data.get("starred_by") print(f"Repository {repo_name} starred by {starred_by}") # Add your business logic here return {"status": "success", "message": "Webhook processed"} ``` **Next.js:** ```typescript title="app/api/webhook/route.ts" // @noErrors import type { NextApiRequest, NextApiResponse } from 'next'; import { TriggerEvent } from '@composio/core'; import crypto from 'crypto'; type GitHubStarEventData = { repository_name: string; repository_url: string; starred_by: string; starred_at: string; }; function verifyWebhookSignature( req: NextApiRequest, body: string ): boolean { const signature = req.headers['webhook-signature'] as string | undefined; const msgId = req.headers['webhook-id'] as string | undefined; const timestamp = req.headers['webhook-timestamp'] as string | undefined; const secret = process.env.COMPOSIO_WEBHOOK_SECRET; if (!signature || !msgId || !timestamp || !secret) { throw new Error('Missing required webhook headers or secret'); } if (!signature.startsWith('v1,')) { throw new Error('Invalid signature format'); } const received = signature.slice(3); const signingString = `${msgId}.${timestamp}.${body}`; const expected = crypto .createHmac('sha256', secret) .update(signingString) .digest('base64'); return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected)); } export default async function webhookHandler(req: NextApiRequest, res: NextApiResponse) { const payload = req.body; if (payload.type === 'github_star_added_event') { const event: TriggerEvent = { type: payload.type, timestamp: payload.timestamp, data: payload.data }; console.log(`Repository ${event.data.repository_name} starred by ${event.data.starred_by}`); // Add your business logic here } res.status(200).json({ status: 'success', message: 'Webhook processed' }); } ``` ## Coming Soon [#coming-soon] ### Local tools [#local-tools] Previously, the Python SDK included *[local tools](https://github.com/ComposioHQ/composio/tree/0.5.0%2Bpost.1/python/composio/tools/local)*. These were tools defined within the SDK and consisted of local shell and code-related tools such as "clipboard", "sqltool", and "shelltool". This feature is currently in development for both Python and TypeScript SDKs, with newly created tools built for improved agent accuracy. This feature is currently in development for both Python and TypeScript SDKs. ## API Endpoints [#api-endpoints] The following table lists important API endpoints that have changed. You can use this reference to quickly find the new v3 API endpoint for migration: > This list is not exhaustive. Please refer to the [API Reference](/reference) for the complete list of endpoints. ### Toolkits (formerly Apps) [#toolkits-formerly-apps] | Previous Endpoint | Current Endpoint | | :--------------------------------- | :-------------------------------- | | `GET /api/v1/apps` | `GET /api/v3/toolkits` | | `GET /api/v1/apps/list/categories` | `GET /api/v3/toolkits/categories` | | `GET /api/v1/apps/{appName}` | `GET /api/v3/toolkits/{slug}` | ### Tools (formerly Actions) [#tools-formerly-actions] | Previous Endpoint | Current Endpoint | | :--------------------------------------------------- | :--------------------------------------------- | | `GET /api/v2/actions` | `GET /api/v3/tools` | | `GET /api/v2/actions/list/enums` | `GET /api/v3/tools/enum` | | `GET /api/v2/actions/{actionId}` | `GET /api/v3/tools/{tool_slug}` | | `POST /api/v2/actions/{actionId}/execute` | `POST /api/v3/tools/execute/{tool_slug}` | | `POST /api/v2/actions/{actionId}/execute/get.inputs` | `POST /api/v3/tools/execute/{tool_slug}/input` | | `POST /api/v2/actions/proxy` | `POST /api/v3/tools/execute/proxy` | ### Auth Configs (formerly Integrations/Connectors) [#auth-configs-formerly-integrationsconnectors] | Previous Endpoint | Current Endpoint | | :-------------------------------------------- | :------------------------------------- | | `GET /api/v1/integrations` | `GET /api/v3/auth_configs` | | `POST /api/v1/integrations` | `POST /api/v3/auth_configs` | | `GET /api/v1/integrations/{integrationId}` | `GET /api/v3/auth_configs/{nanoid}` | | `PATCH /api/v1/integrations/{integrationId}` | `PATCH /api/v3/auth_configs/{nanoid}` | | `DELETE /api/v1/integrations/{integrationId}` | `DELETE /api/v3/auth_configs/{nanoid}` | | `POST /api/v2/integrations/create` | `POST /api/v3/auth_configs` | ### Connected Accounts (formerly Connections) [#connected-accounts-formerly-connections] | Previous Endpoint | Current Endpoint | | :--------------------------------------------------------------- | :------------------------------------------------- | | `GET /api/v1/connectedAccounts` | `GET /api/v3/connected_accounts` | | `POST /api/v1/connectedAccounts` | `POST /api/v3/connected_accounts` | | `POST /api/v2/connectedAccounts/initiateConnection` | `POST /api/v3/connected_accounts` | | `GET /api/v1/connectedAccounts/{connectedAccountId}` | `GET /api/v3/connected_accounts/{nanoid}` | | `DELETE /api/v1/connectedAccounts/{connectedAccountId}` | `DELETE /api/v3/connected_accounts/{nanoid}` | | `POST /api/v1/connectedAccounts/{connectedAccountId}/disable` | `PATCH /api/v3/connected_accounts/{nanoId}/status` | | `POST /api/v1/connectedAccounts/{connectedAccountId}/enable` | `PATCH /api/v3/connected_accounts/{nanoId}/status` | | `POST /api/v1/connectedAccounts/{connectedAccountId}/reinitiate` | `POST /api/v3/connected_accounts/{nanoid}/refresh` | ### Triggers [#triggers-1] | Previous Endpoint | Current Endpoint | | :---------------------------------------------------------------- | :---------------------------------------------------- | | `GET /api/v1/triggers` | `GET /api/v3/triggers_types` | | `GET /api/v1/triggers/list/enums` | `GET /api/v3/triggers_types/list/enum` | | `GET /api/v2/triggers/{triggerName}` | `GET /api/v3/triggers_types/{slug}` | | `GET /api/v1/triggers/active_triggers` | `GET /api/v3/trigger_instances/active` | | `POST /api/v1/triggers/enable/{connectedAccountId}/{triggerName}` | `POST /api/v3/trigger_instances/{slug}/upsert` | | `DELETE /api/v1/triggers/instance/{triggerInstanceId}` | `DELETE /api/v3/trigger_instances/manage/{triggerId}` | | `PATCH /api/v1/triggers/instance/{triggerId}/status` | `PATCH /api/v3/trigger_instances/manage/{triggerId}` | --- # Security (/docs/security/overview) Composio is built with security at its core. We use least-privilege defaults, isolate every organization and project, encrypt credentials, and give you controls over what we store. ## Compliance and the Trust Center [#compliance-and-the-trust-center] Composio is SOC 2 Type II compliant. For our latest reports and certifications (the SOC 2 Type II report, our sub-processor list, and more), visit the [Composio Trust Center](https://trust.composio.dev). ## Isolation and access control [#isolation-and-access-control] * Organizations and projects isolate your resources. Data from one project is not visible to another. * API keys are scoped, support per-key IP allowlisting, and can opt in to capabilities such as Proxy Execute at creation. * Multi-factor authentication (MFA) is available for Dashboard sign-in and can be enforced by an organization admin. ## Credential protection [#credential-protection] * Connected-account credentials, auth configs, and API keys are encrypted at rest using AES-256-GCM, and all traffic is encrypted in transit using TLS. * Connected-account tokens are redacted by default in API responses, for both Composio-managed and custom auth configs. To act on a provider directly, use [Proxy Execute](/docs/extending-sessions/proxy-execute). * Webhook deliveries are signed; verify the `webhook-signature` header when handling trigger events. For custom OAuth webhook setup, see [Custom OAuth webhooks](/docs/setting-up-triggers/custom-oauth-webhooks). ## Your responsibilities [#your-responsibilities] Composio executes the tools and connections you configure. You control which toolkits are enabled, which accounts are connected, and what your agents are allowed to do. Review the access you grant, and treat connected-account scopes as you would any production credential. ## Reporting a vulnerability [#reporting-a-vulnerability] To report a security issue, contact `security@composio.dev`. Please do not disclose vulnerabilities publicly until we have addressed them. ## Related resources [#related-resources] * [Data retention](/docs/security/data-retention): what we store, for how long, and how to stop storing payloads. * [Composio Trust Center](https://trust.composio.dev): security certifications and compliance. --- # Data retention (/docs/security/data-retention) Composio stores audit logs for tool executions so you can observe and debug your agents. You control whether the request and response payloads for each call are stored. ## Customer data vs. your end users' data [#customer-data-vs-your-end-users-data] Throughout this guide, *you* are the Composio customer. *Your end users* are the people your agent acts on behalf of. Each end user is identified by the `user_id` you provide. Composio stores this value as supplied and uses it to associate connected accounts, sessions, and execution logs. Tool requests and responses may contain additional end-user data. The **Log storage** setting controls whether these payloads are retained; it does not remove the `user_id` or other audit metadata. ## What Composio stores [#what-composio-stores] Composio creates logs for tool executions and trigger events. These logs contain: * For tool executions, the toolkit and action, execution status, connection and auth-config IDs, supplied `user_id`, timing, and runtime/source. * By default, the **request arguments** and **response data** for each tool execution. * For trigger events, similar audit metadata and, by default, the trigger payloads. ## How long logs are retained [#how-long-logs-are-retained] Tool execution logs and trigger event logs are retained for up to **one year**, after which they are automatically deleted. ## How long files are available [#how-long-files-are-available] When a tool uses or returns a file (an attachment, image, document, export, and so on), Composio stages it in temporary object storage and shares it through a presigned URL. The URL is valid for **1 hour by default**. You can configure this URL TTL per project in **Project Settings**, up to 24 hours. Presigned URL expiry and file deletion are separate. When the URL expires, the link stops resolving, but that does not delete the underlying object. Files staged for tool execution are automatically deleted from temporary object storage after **24 hours**. Workbench storage has two separate lifecycles: * Files written to `/mnt/files` are backed by temporary object storage and are automatically deleted after **24 hours**. * Other sandbox files, variables, and runtime state are temporary and may be cleared after approximately **12 hours** of inactivity. ## Choose what we store: the Log storage setting [#choose-what-we-store-the-log-storage-setting] You control whether call payloads are stored for each project in **Settings → General → Log storage**: * **Store all logs** (default) — Composio stores the full request and response payloads in the execution log. * **Don't store data** — Composio does not store the request arguments or response data from your tool calls. It keeps only the audit record: which tool ran, when it ran, whether it succeeded, the relevant IDs, and timing information. With **Don't store data**, Composio keeps an audit trail but does not retain your tool-call payloads (including your end users' data) in its logs. This setting also applies to [Proxy Execute](/docs/extending-sessions/proxy-execute). If Composio cannot retrieve your project's log-storage setting, Proxy Execute does not store the request or response payload. Log storage set to "Don't store data" in Project Settings, General > To stop storing payloads, open the Dashboard, choose your project, go to **Settings → General → Log storage**, and select **Don't store data**. ### Changing the setting affects new calls [#changing-the-setting-affects-new-calls] You can switch between the two log-storage options at any time. Each change applies only to new calls: * Selecting **Don't store data** stops Composio from storing payloads for new calls. It does **not** delete payloads that were already stored: existing logs stay available until they age out under the one-year retention window. * Selecting **Store all logs** again resumes storing payloads for new calls. It does **not** backfill the period while payload storage was disabled. Payloads for calls made while storage was disabled were never stored and cannot be recovered later. ## Where your data goes [#where-your-data-goes] Choosing **Don't store data** stops Composio from persisting your payloads, but data may still pass through the following locations during execution: * The **destination provider** that receives the tool call. * **Composio's execution infrastructure** while the call runs. * **Temporary object storage** for files used or returned by tool calls. Presigned URL expiry is configurable per project, while the underlying staged file is deleted after 24 hours. See [How long files are available](#how-long-files-are-available). * Your configured **trigger destinations**, which receive trigger events in real time. * An isolated third-party **code sandbox**, if you use Workbench or remote code execution. Ordinary Python or shell execution does not inherently send code or sandbox files to an LLM provider. Model-provider processing occurs only in the cases described below. ### When Workbench uses an LLM [#when-workbench-uses-an-llm] When a task requires advanced processing, the agent or MCP client may use Workbench and call `invoke_llm` from the submitted code—for example, to summarize, analyze, extract, or generate content. Only the information passed to `invoke_llm` is sent to the model provider; sandbox files and previous results are not sent unless the submitted code includes them. There is one separate case: if submitted Python contains a syntax error and automatic repair is enabled, Composio may send the code and error details to the configured model provider to fix it before execution. Valid Python and ordinary shell commands do not trigger this repair. So "Don't store data" controls what Composio retains, not whether data is processed during execution. For the current list of our sub-processors and our data-handling terms, see the [Composio Trust Center](https://trust.composio.dev/subprocessors). ## Stronger guarantees [#stronger-guarantees] If your organization needs a contractual zero-data-retention arrangement or shorter retention windows, [contact sales](https://composio.dev/contact?utm_source=docs). For our data-handling policies and sub-processor list, see the [Composio Trust Center](https://trust.composio.dev). --- # Claude Code Plugin (/docs/claude-code-plugin) The **Composio plugin for Claude Code** lets Claude act on 1,000+ apps — send the Slack message, open the Linear issue, check your calendar, draft the email. Your agent decides what to do; Composio handles the rest: OAuth, permissions, and finding the right tool for each task. No API keys, no config files. > Using Codex? Follow the [Codex setup](/docs/agent-plugins#configure-one-agent). If you explicitly want MCP in Cursor, Claude Desktop, or another MCP client, use [Composio Connect](/docs/composio-connect). ## Install [#install] #### Add the Composio marketplace In Claude Code, run: ```bash /plugin marketplace add ComposioHQ/composio-plugin-cc ``` #### Install the plugin ```bash /plugin install composio@composio ``` Restart Claude Code (or run `/reload-plugins`) when prompted. #### Ask Claude to do something Try: *"Star `composiohq/composio` on GitHub."* The first time, Claude installs the Composio CLI if it's missing, signs you in with `composio login`, and gives you an OAuth link for GitHub. Approve it in your browser and Claude runs the action. > Prefer to set things up ahead of time? Run `curl -fsSL https://composio.dev/install | sh` to install the CLI and configure your shell, then open a new terminal and run `composio login`. ## What you can do [#what-you-can-do] Naming the app in your prompt keeps tool search scoped and the run reliable. **Act on any connected app:** ```text What's on my Google Calendar for tomorrow? Add an event for lunch at 12PM. ``` **Run cross-app workflows** — reads feed the write: ```text Take the latest merged PR in acme/app, open a Linear issue summarizing it, and post the issue link to #eng in Slack. ``` **Fan out reads, then summarize:** ```text In parallel, fetch my last 10 Gmail emails, my open Linear issues, and today's Google Calendar events. Redact personal info, then give me a concise summary. ``` ## Connecting apps [#connecting-apps] Apps connect on demand — the first task that needs one hands you an OAuth link. To connect an app ahead of time: ```bash /composio-connect linear ``` Works for any of the 1,000+ supported apps — `slack`, `github`, `gmail`, `notion`, `linear`, `hubspot`, and more. ## Team setup [#team-setup] To pre-install the plugin for everyone on your team, add this to your project's `.claude/settings.json`: ```json title=".claude/settings.json" { "extraKnownMarketplaces": { "composio": { "source": { "source": "github", "repo": "ComposioHQ/composio-plugin-cc" } } }, "enabledPlugins": { "composio@composio": true } } ``` Anyone who clones the repo and opens it in Claude Code will be prompted to enable the plugin. See the Claude Code [plugin scopes](https://docs.claude.com/en/docs/claude-code/plugins-reference#plugin-installation-scopes) reference for `user` vs `project` vs `local` scope behavior. ## Updating [#updating] To pull the latest plugin release: ```bash /plugin marketplace update composio /reload-plugins ``` New capabilities usually ship in the Composio CLI itself — `composio upgrade` picks them up without touching the plugin. ## Source code [#source-code] The plugin is open source: [ComposioHQ/composio-plugin-cc](https://github.com/ComposioHQ/composio-plugin-cc). Issues and PRs welcome. ## Troubleshooting [#troubleshooting] For plugin issues — install errors, marketplace not updating — see the [Claude Code plugin troubleshooting docs](https://docs.claude.com/en/docs/claude-code/plugins-reference#common-issues). For auth or connection issues, see the [CLI reference](/docs/cli) or run `composio --help`. --- # Single Toolkit MCP (/docs/single-toolkit-mcp) > For most use cases, use a regular [session](/docs/configuring-sessions) instead. Sessions provide dynamic tool access and a much better MCP experience with context management handled by us. ## Install the SDK [#install-the-sdk] **Python:** **TypeScript:** ## Create an MCP server [#create-an-mcp-server] #### Initialize Composio [#initialize-composio] **Python:** ```python from composio import Composio composio = Composio(api_key="YOUR_API_KEY") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); ``` #### Create server configuration [#create-server-configuration] > **Before you begin:** [Create an auth configuration](/docs/auth-configuration/custom-auth-configs) for your toolkit. **Python:** ```python server = composio.mcp.create( name="my-gmail-server", toolkits=[{ "toolkit": "gmail", "auth_config": "ac_xyz123" }], allowed_tools=["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"] ) print(f"Server created: {server.id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const server = await composio.mcp.create("my-gmail-server", { toolkits: [ { authConfigId: "ac_xyz123", toolkit: "gmail" } ], allowedTools: ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"] }); console.log(`Server created: ${server.id}`); ``` > You can also create and manage MCP configs from the [Composio dashboard](https://dashboard.composio.dev/~/org/connect/clients?utm_source=docs\&utm_medium=content\&utm_campaign=docs-single-toolkit-mcp). #### Generate user URLs [#generate-user-urls] > Users must authenticate with the toolkits configured in your MCP server first. See [authentication](/docs/authentication) for details. **Python:** ```python instance = composio.mcp.generate(user_id="user-123", mcp_config_id=server.id) print(f"MCP Server URL: {instance['url']}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const server = { id: 'my-gmail-server' }; const instance = await composio.mcp.generate("user-123", server.id); console.log("MCP Server URL:", instance.url); ``` #### Use with AI providers [#use-with-ai-providers] > Pass an `x-api-key` header when connecting to Composio MCP. This is required when `require_mcp_api_key` is enabled (default for newly created organizations). **OpenAI (Python):** ```python from openai import OpenAI client = OpenAI(api_key="your-openai-api-key") mcp_server_url = "https://backend.composio.dev/v3/mcp/YOUR_SERVER_ID?user_id=YOUR_USER_ID" mcp_headers = {"x-api-key": "YOUR_COMPOSIO_API_KEY"} response = client.responses.create( model="gpt-5", tools=[{ "type": "mcp", "server_label": "composio-server", "server_url": mcp_server_url, "headers": mcp_headers, "require_approval": "never", }], input="What are my latest emails?", ) print(response.output_text) ``` **Anthropic (Python):** ```python from anthropic import Anthropic client = Anthropic(api_key="your-anthropic-api-key") mcp_server_url = "https://backend.composio.dev/v3/mcp/YOUR_SERVER_ID?user_id=YOUR_USER_ID" mcp_headers = {"x-api-key": "YOUR_COMPOSIO_API_KEY"} response = client.beta.messages.create( model="claude-sonnet-4-6", max_tokens=1000, messages=[{"role": "user", "content": "What are my latest emails?"}], mcp_servers=[{ "type": "url", "url": mcp_server_url, "name": "composio-mcp-server", "headers": mcp_headers, }], betas=["mcp-client-2025-04-04"] ) print(response.content) ``` **Mastra (TypeScript):** ```typescript import { MCPClient } from "@mastra/mcp"; import { openai } from "@ai-sdk/openai"; import { Agent } from "@mastra/core/agent"; const MCP_URL = "https://backend.composio.dev/v3/mcp/YOUR_SERVER_ID?user_id=YOUR_USER_ID"; const MCP_HEADERS = { "x-api-key": "YOUR_COMPOSIO_API_KEY" }; const client = new MCPClient({ id: "mcp-client", servers: { composio: { url: new URL(MCP_URL), headers: MCP_HEADERS }, } }); const agent = new Agent({ id: "assistant", name: "Assistant", instructions: "You are a helpful assistant that can read and manage emails.", model: openai("gpt-5.4"), tools: await client.getTools() }); const res = await agent.generate("What are my latest emails?"); console.log(res.text); ``` ## Server management [#server-management] ### List servers [#list-servers] **Python:** ```python servers = composio.mcp.list() print(f"Found {len(servers['items'])} servers") # Filter by toolkit gmail_servers = composio.mcp.list(toolkits="gmail", limit=20) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const servers = await composio.mcp.list({ toolkits: [], authConfigs: [], limit: 10, page: 1 }); console.log(`Found ${servers.items.length} servers`); // Filter by toolkit const gmailServers = await composio.mcp.list({ toolkits: ["gmail"], authConfigs: [], limit: 20, page: 1 }); ``` ### Get server details [#get-server-details] **Python:** ```python server = composio.mcp.get("mcp_server_id") print(f"Server: {server.name}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const server = await composio.mcp.get("mcp_server_id"); console.log(`Server: ${server.name}`); ``` ### Update a server [#update-a-server] **Python:** ```python updated = composio.mcp.update( server_id="mcp_server_id", name="updated-name", allowed_tools=["GMAIL_FETCH_EMAILS", "GMAIL_SEARCH_EMAILS"] ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const updated = await composio.mcp.update("mcp_server_id", { name: "updated-name", allowedTools: ["GMAIL_FETCH_EMAILS", "GMAIL_SEARCH_EMAILS"] }); ``` ### Delete a server [#delete-a-server] **Python:** ```python result = composio.mcp.delete("mcp_server_id") if result['deleted']: print("Server deleted") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const result = await composio.mcp.delete("mcp_server_id"); if (result.deleted) { console.log("Server deleted"); } ``` ## Next [#next] - [Providers](/docs/providers): Use with Anthropic, OpenAI, and other frameworks --- # Knowledge Hub navigation --- - https://docs.composio.dev/kb - https://docs.composio.dev/kb/search - https://docs.composio.dev/kb/topic/authentication-and-connected-accounts - https://docs.composio.dev/kb/topic/tools-actions-and-execution - https://docs.composio.dev/kb/topic/triggers-and-workflows - https://docs.composio.dev/kb/topic/sdk-api-and-mcp - https://docs.composio.dev/kb/topic/account-billing-and-security - https://docs.composio.dev/kb/toolkits - https://docs.composio.dev/kb/toolkit/gmail - https://docs.composio.dev/kb/toolkit/github - https://docs.composio.dev/kb/toolkit/googlecalendar - https://docs.composio.dev/kb/toolkit/notion - https://docs.composio.dev/kb/toolkit/googlesheets - https://docs.composio.dev/kb/toolkit/slack - https://docs.composio.dev/kb/toolkit/supabase - https://docs.composio.dev/kb/toolkit/outlook - https://docs.composio.dev/kb/toolkit/perplexityai - https://docs.composio.dev/kb/toolkit/twitter - https://docs.composio.dev/kb/toolkit/googledrive - https://docs.composio.dev/kb/toolkit/googledocs - https://docs.composio.dev/kb/toolkit/hubspot - https://docs.composio.dev/kb/toolkit/linear - https://docs.composio.dev/kb/toolkit/airtable - https://docs.composio.dev/kb/toolkit/serpapi - https://docs.composio.dev/kb/toolkit/jira - https://docs.composio.dev/kb/toolkit/firecrawl - https://docs.composio.dev/kb/toolkit/tavily - https://docs.composio.dev/kb/toolkit/youtube - https://docs.composio.dev/kb/toolkit/slackbot - https://docs.composio.dev/kb/toolkit/canvas - https://docs.composio.dev/kb/toolkit/googletasks - https://docs.composio.dev/kb/toolkit/discord - https://docs.composio.dev/kb/toolkit/figma - https://docs.composio.dev/kb/toolkit/reddit - https://docs.composio.dev/kb/toolkit/wrike - https://docs.composio.dev/kb/toolkit/snowflake - https://docs.composio.dev/kb/toolkit/microsoft_teams - https://docs.composio.dev/kb/toolkit/asana - https://docs.composio.dev/kb/toolkit/shopify - https://docs.composio.dev/kb/toolkit/linkedin - https://docs.composio.dev/kb/toolkit/google_maps - https://docs.composio.dev/kb/toolkit/one_drive - https://docs.composio.dev/kb/toolkit/docusign - https://docs.composio.dev/kb/toolkit/discordbot - https://docs.composio.dev/kb/toolkit/salesforce - https://docs.composio.dev/kb/toolkit/calendly - https://docs.composio.dev/kb/toolkit/trello - https://docs.composio.dev/kb/toolkit/apollo - https://docs.composio.dev/kb/toolkit/posthog - https://docs.composio.dev/kb/toolkit/clickup - https://docs.composio.dev/kb/toolkit/stripe - https://docs.composio.dev/kb/toolkit/klaviyo - https://docs.composio.dev/kb/toolkit/mailchimp - https://docs.composio.dev/kb/toolkit/attio - https://docs.composio.dev/kb/toolkit/googlemeet - https://docs.composio.dev/kb/toolkit/zoho - https://docs.composio.dev/kb/toolkit/dropbox - https://docs.composio.dev/kb/toolkit/confluence - https://docs.composio.dev/kb/toolkit/ahrefs - https://docs.composio.dev/kb/toolkit/googlebigquery - https://docs.composio.dev/kb/toolkit/monday - https://docs.composio.dev/kb/toolkit/pipedrive - https://docs.composio.dev/kb/toolkit/whatsapp - https://docs.composio.dev/kb/toolkit/zendesk - https://docs.composio.dev/kb/toolkit/googlesuper - https://docs.composio.dev/kb/toolkit/browser_tool - https://docs.composio.dev/kb/toolkit/rocketlane - https://docs.composio.dev/kb/toolkit/zoom - https://docs.composio.dev/kb/toolkit/servicenow - https://docs.composio.dev/kb/toolkit/googleads - https://docs.composio.dev/kb/toolkit/pagerduty - https://docs.composio.dev/kb/toolkit/share_point - https://docs.composio.dev/kb/toolkit/launch_darkly - https://docs.composio.dev/kb/toolkit/netsuite - https://docs.composio.dev/kb/toolkit/zoho_books - https://docs.composio.dev/kb/toolkit/facebook - https://docs.composio.dev/kb/toolkit/canva - https://docs.composio.dev/kb/toolkit/webflow - https://docs.composio.dev/kb/toolkit/google_analytics - https://docs.composio.dev/kb/toolkit/ynab - https://docs.composio.dev/kb/toolkit/kommo - https://docs.composio.dev/kb/toolkit/gong - https://docs.composio.dev/kb/toolkit/xero - https://docs.composio.dev/kb/toolkit/zoho_mail - https://docs.composio.dev/kb/toolkit/intercom - https://docs.composio.dev/kb/toolkit/databricks - https://docs.composio.dev/kb/toolkit/daytona - https://docs.composio.dev/kb/toolkit/digital_ocean - https://docs.composio.dev/kb/toolkit/excel - https://docs.composio.dev/kb/toolkit/fathom - https://docs.composio.dev/kb/toolkit/gemini - https://docs.composio.dev/kb/toolkit/gitlab - https://docs.composio.dev/kb/toolkit/google_classroom - https://docs.composio.dev/kb/toolkit/googleslides - https://docs.composio.dev/kb/toolkit/granola_mcp - https://docs.composio.dev/kb/toolkit/instagram - https://docs.composio.dev/kb/toolkit/instantly - https://docs.composio.dev/kb/toolkit/kickbox - https://docs.composio.dev/kb/toolkit/marketstack - https://docs.composio.dev/kb/toolkit/onenote - https://docs.composio.dev/kb/toolkit/odoo - https://docs.composio.dev/kb/toolkit/openai - https://docs.composio.dev/kb/toolkit/quickbooks - https://docs.composio.dev/kb/toolkit/ramp - https://docs.composio.dev/kb/toolkit/snapchat - https://docs.composio.dev/kb/toolkit/spotify - https://docs.composio.dev/kb/toolkit/strava - https://docs.composio.dev/kb/toolkit/telegram - https://docs.composio.dev/kb/toolkit/ticktick - https://docs.composio.dev/kb/toolkit/tiktok - https://docs.composio.dev/kb/toolkit/workday --- # Knowledge Base --- # Knowledge Base (/kb) Verified troubleshooting guides, operational answers, and known-good patterns from Composio support. --- # Consumer and Developer Project Boundaries (/kb/guide/consumer-project-boundaries-and-auth-selection) Composio organizations have separate developer and consumer project surfaces. The developer dashboard/API shows developer projects. The consumer dashboard and consumer MCP / Composio MCP / Composio For You clients use a separate consumer project that customers usually do not see directly. Developer-project auth configs and connected accounts are not available in the consumer project. Consumer-project connections are not available in the developer project. If you created an auth config or connected an account in the developer dashboard but cannot use it in Claude, ChatGPT, Codex, Cursor, Composio For You, or another consumer MCP client, connect the account through the consumer flow instead. Consumer MCP auth has two common paths: 1. Use the consumer MCP URL directly. If the MCP client supports auth, it can trigger the Composio auth flow, open a popup, let the customer authenticate, and let them select the organization. 2. Use the consumer API key when the MCP client does not support auth and only supports API keys/headers. Copy the key from the consumer dashboard and pass it as the `x-consumer-api-key` header. Under the hood, a consumer-scoped MCP session is created for the specific user and allowed tools. The For You connection flow uses the auth config in the consumer project. Its current behavior is: 1. When a Composio-managed auth app is available, the app uses that managed config and does not expose provider credentials for editing. 2. When no managed app is available, **Manage Auth** appears for toolkits with editable customer-owned credentials or multiple auth schemes. Enter the provider client ID/secret or other required fields there and register the callback URI shown by the current form. 3. Provider scopes are determined by the selected auth scheme and its current config. A managed app is limited to its approved scope set; customer-owned apps must configure and verify their scopes with the provider. ## Rotate the Connect MCP consumer key [#rotate-the-connect-mcp-consumer-key] These steps apply to a Connect consumer key with the `ck_*` prefix, sent as `x-consumer-api-key`. They do not apply to a Platform Project API key with the `ak_*` prefix. Reconnecting Gmail, Calendar, or another individual app does not rotate the consumer key. 1. Open For You / Connect. 2. Select **Settings** in the left sidebar. 3. Open **Sessions & API Key**. 4. Select **Regenerate** next to “Your API Key” and confirm. 5. Update every MCP client that uses `https://connect.composio.dev/mcp` with the new `x-consumer-api-key` value. Regeneration immediately invalidates the old consumer key. If the button is missing even though you are in the correct workspace with write access, or you need help investigating suspicious usage, contact Composio support for account-level assistance. ## Workspace members do not automatically share For You connections [#workspace-members-do-not-automatically-share-for-you-connections] In the normal For You/Connect MCP flow, connected accounts belong to the member who authorized them. Another teammate using their own Connect MCP endpoint or `ck_*` consumer key resolves to their own connected accounts, not yours. * Admins can manage workspace settings and members, but their own Connect MCP session does not automatically use another member's accounts. * Members can connect and use their own accounts. * Viewers cannot connect or invoke tools from the For You surface. Raw `ak_*` Project API keys are different from `ck_*` consumer keys and must be treated as privileged project secrets. Explicitly shared or pinned connections are also a separate configuration from ordinary member-scoped connections. ## Shared connections must be explicitly allowed and pinned [#shared-connections-must-be-explicitly-allowed-and-pinned] A normal `PRIVATE` connection belongs to the user who created it. A `SHARED` connection can be used by other user IDs only when its ACL allows them and the connection is explicitly pinned into the session by connected-account ID. Shared connections are deny-by-default and are never selected implicitly. Follow the [Shared Connections guide](https://docs.composio.dev/docs/shared-connections#shared-vs-private) for the current ACL and session configuration. --- # Delete a Composio organization (/kb/guide/dashboard-account-deletion) ## Organization admins can use Delete this organization [#organization-admins-can-use-delete-this-organization] In Platform, open **Settings → Organization Settings → General**. In For You, open **Settings → General**. Under **Delete this organization**, select **Delete organization** and complete the confirmation shown by the dashboard. The control is disabled for non-admins. A non-admin should contact an organization admin. The current warning states that deletion permanently removes the organization, its projects, connected accounts, API keys, and logs, so confirm the organization before proceeding. If upstream provider credentials also need to be invalidated, revoke the connected accounts where supported and remove the app or rotate the credential in the provider's own settings when necessary. Never send credentials to Composio support. --- # Manage Platform auth configs (/kb/guide/dashboard-auth-configs-navigation) ## Create an auth config from the selected Platform project [#create-an-auth-config-from-the-selected-platform-project] Open **Platform → Auth Configs → Create Auth Config**, choose the toolkit and supported authentication method, then select managed authentication when it is available or enter customer-owned credentials. For custom OAuth, register the exact callback URI shown by the current dashboard in the provider app; do not copy a callback URI from an old example. Auth configs belong to one Platform project. If a config or connection is missing, verify the selected organization and project before recreating it. ## Connect Account on an auth config is a Playground test connection [#connect-account-on-an-auth-config-is-a-playground-test-connection] Open an auth config and select **Connect Account** to authenticate the project's Playground user for testing. This control does not ask for an application user ID. To connect an actual application user, create a hosted connection link through the SDK or API with that application's stable `user_id` and the intended auth config. ## Manage Config changes future authentication behavior [#manage-config-changes-future-authentication-behavior] Use **Manage Config** to inspect the enabled state, credentials, and available scope or execution settings for that config type. Changing credentials or scopes can require users to create a fresh connection before the change is reflected in their provider grant. Review dependent connections, sessions, and triggers before disabling or deleting a config. --- # Composio For You navigation (/kb/guide/dashboard-for-you-navigation) ## Use Connect Apps for provider accounts and Connect my agent for clients [#use-connect-apps-for-provider-accounts-and-connect-my-agent-for-clients] The current For You sidebar contains **Home**, **Connect Apps**, **Connect my agent**, and **Help**. * Open **Connect Apps**, select an app, and choose **Connect** to authenticate a provider account. * Open **Connect my agent** and select the target agent or MCP client for the current setup instructions. Copy the MCP URL, header, or key shown on that page rather than transferring configuration syntax from another client. * Open **Help** when the user needs the support route rather than self-service product guidance. ## For You settings include Sessions & API Key [#for-you-settings-include-sessions--api-key] For You settings currently contain **General**, **Members**, **Sessions & API Key**, and **Billing**. Use **Sessions & API Key** to inspect the current consumer connection instructions or regenerate the consumer key. Regeneration invalidates the previous key, so update every client that uses it. Never ask a customer to paste the key into a support conversation. For project auth configs, project users, triggers, logs, or `ak_...` project API keys, switch to Platform instead. --- # Dashboard Log Storage (/kb/guide/dashboard-log-storage) ## “Don't store data” removes new payload content, not the audit row [#dont-store-data-removes-new-payload-content-not-the-audit-row] New tool executions can still appear in Tool Logs with audit metadata such as tool, status, timestamp, duration, and related identifiers. With **Don't store data** enabled, their request arguments and response payload content are not stored in those rows. Changing the setting does not retroactively erase older payloads. Run a new test after changing it and inspect that new row. If new request or response content remains visible, contact support with the timestamp and log reference. This setting does not define every contractual retention or deletion window. Use Composio's approved security and privacy documentation for those questions. --- # Dashboard MFA Setup (/kb/guide/dashboard-login-restrictions) Use this when a user cannot complete authenticator-app enrollment from the QR code in Account Settings. ## Use the manual setup key when QR scanning does not complete [#use-the-manual-setup-key-when-qr-scanning-does-not-complete] The MFA setup screen shows a QR code and a **View setup key** option. If the QR code cannot be scanned or the setup screen expires, open **View setup key** and enter that key manually in the authenticator app. Then enter the resulting six-digit passcode in Composio to finish enrollment. After enrollment is complete, resetting the setup key requires removing or resetting the MFA factor and enrolling the authenticator again. --- # Composio dashboard navigation (/kb/guide/dashboard-navigation-overview) ## Choose Platform for developer projects and For You for personal agent connections [#choose-platform-for-developer-projects-and-for-you-for-personal-agent-connections] **Platform** contains developer projects, Playground, project API keys, toolkits, skills, users, sessions, auth configs, triggers, logs, and project settings. **For You** connects a person's apps to supported agents and MCP clients. Resources in one surface do not automatically appear in the other. Use the product switcher when a user is looking for a personal app connection inside Platform or for project resources inside For You. ## Current Platform project navigation [#current-platform-project-navigation] The Platform project sidebar contains **Getting Started**, **Playground**, **API Keys**, **Toolkits**, **Skills**, **Users**, **Sessions**, **Auth Configs**, **Triggers**, and **Logs**. Pinned destinations include **Support**, **Documentation**, and **Settings**. * Use **Playground** to test a session with selected toolkits, tools, auth configs, and connected accounts. * Use **Toolkits** to inspect current toolkit versions, tool and trigger schemas, and supported auth schemes. * Use **Users** and **Sessions** to correlate a project user with connections, executions, and session restrictions. * Use **Logs** for the request, response, error, version, and Log ID of a tool or trigger execution. ## Organization and project settings are separate [#organization-and-project-settings-are-separate] Project settings include **General**, **API Keys**, **Webhooks**, **White Labeling**, and **Usage**. Organization settings include **General**, **Members**, **Billing**, **Usage**, and **Account Settings**. Verify the page heading before changing or deleting a resource. --- # Organization Members and Administrators (/kb/guide/dashboard-org-members) ## Transfer organization control to another user [#transfer-organization-control-to-another-user] Transfer organization control by assigning the **Admin** role: 1. Open **Organization Settings → Team Members**. 2. Invite the new user and assign the **Admin** role. 3. Have the new user accept the invitation and sign in. 4. If the previous admin should no longer have control, the new admin can lower or remove that person afterward. A user cannot normally remove or change their own Team Members row. If the existing administrator cannot access the organization, the invitation or role selector fails, or the change could leave the organization without an administrator, contact support. Removing the previous user does not delete the organization, but API keys owned by that removed account can stop working. Rotate or replace those keys before removal when necessary. --- # Platform project settings (/kb/guide/dashboard-project-settings-navigation) ## Project settings control one project [#project-settings-control-one-project] Open **Platform → Settings** for the selected project. The current project settings pages are **General**, **API Keys**, **Webhooks**, **White Labeling**, and **Usage**. * **API Keys** creates or revokes project keys and manages any key-level IP allowlist. Copy a newly created secret into the customer's secret manager; never ask for it in support. * **Webhooks** manages the project webhook endpoint and signing secret. * **White Labeling** controls the hosted authentication screen. Provider OAuth consent-screen branding still requires the customer's own provider app. * **Usage** shows project-level usage rather than organization-wide usage. ## Organization settings control the organization [#organization-settings-control-the-organization] The organization settings pages are **General**, **Members**, **Billing**, **Usage**, and **Account Settings**. Use them for organization identity, membership, plan and usage information, account security, and organization deletion. Confirm whether the customer intends to change one project or the whole organization before directing them to a destructive control. --- # Debug Platform tools, triggers, users, and sessions (/kb/guide/dashboard-tool-trigger-logs-navigation) ## Use Logs for execution evidence [#use-logs-for-execution-evidence] Open **Platform → Logs** and choose the tool or trigger log view. Filter by the smallest known non-secret identifier, then open the row to inspect its status, toolkit, action or trigger, version, user, connection, request/response or provider error, timing, and correlation IDs. Use a **Log ID** for a tool execution, a **Trigger ID** plus Log ID for a trigger event, and a **Session ID** for session behavior. Never request API keys, access or refresh tokens, provider client secrets, webhook secrets, or passwords. ## Use Users and Sessions to explain retrieval and execution context [#use-users-and-sessions-to-explain-retrieval-and-execution-context] Open **Users** to find a project user and its connected accounts, triggers, sessions, and filtered logs. Open **Sessions** to inspect session toolkits, connection behavior, and execution timeline. An active connection elsewhere in the organization does not prove it was eligible for this session: project, `user_id`, toolkit restrictions, auth-config selection, and explicit connected account selection all affect resolution. ## Disable a trigger when the goal is to pause it [#disable-a-trigger-when-the-goal-is-to-pause-it] Open **Triggers** to inspect status and related logs. Disable a trigger when it should pause temporarily; delete it only when the subscription should be removed. Before recreating a trigger, verify the selected project, user, connected account, trigger type, and current provider event support. --- # Current Support FAQs (/kb/guide/faqs) ## Request a toolkit, tool, trigger, or partnership [#request-a-toolkit-tool-trigger-or-partnership] The same public request board covers all three request types: * a new toolkit or integration; * a missing tool or action in an existing toolkit; * a missing trigger or event in an existing toolkit. Submit any of these requests at [https://request.composio.dev/boards/tool-requests](https://request.composio.dev/boards/tool-requests). Include the provider or toolkit, the exact tool/action/API endpoint or trigger/event, and your use case. The request board is the source of truth for status; an ETA is not guaranteed. If your company wants its own product added to Composio, apply with a company work email and product details at [https://composio.dev/partnerships#apply](https://composio.dev/partnerships#apply). The public partnership form asks for company, product, and proposed-journey context. ## Security, privacy, data-retention, and compliance information [#security-privacy-data-retention-and-compliance-information] Use [https://trust.composio.dev/](https://trust.composio.dev/) for general security, privacy, data-retention, audit, and compliance information. If the Trust Center does not answer your question, contact Composio support. Use the documented Dashboard self-service path for ordinary organization deletion. Report potential vulnerabilities privately through the security-reporting channels below; direct legal requests, data-erasure requests beyond the self-service flow, and account-specific access questions to Composio support. ## Security reporting [#security-reporting] If you believe you have found a potential security vulnerability in Composio, please report it privately through the channels in our [security policy](https://github.com/ComposioHQ/composio/security/policy). A private GitHub Security Advisory is the preferred route, with `security@composio.dev` available as an email alternative. Include enough detail to help the team reproduce and assess the finding, but do not include customer data, credentials, or other secrets. ## Google `access_not_configured` requires a Workspace for Education administrator [#google-access_not_configured-requires-a-workspace-for-education-administrator] Google documents `400 access_not_configured` as a Workspace for Education app access-policy error. The institution's Workspace administrator must configure access for the app; changing Composio scopes or repeatedly reconnecting does not resolve that policy decision. If the organization allows users to request access, the user can submit the request from Google's error page. An administrator with the required Security settings privilege can review pending requests or configure the exact OAuth client under **Security → Access and data control → API controls → Manage App Access**. The administrator should use the access level and organizational unit appropriate for the institution. Google says policy changes can take up to 24 hours, though they usually apply sooner. Do not generalize this code to every Google Workspace account. Distinguish it from `admin_policy_enforced`, `access_denied`, and unverified-app errors before giving instructions. --- # Hermes MCP (/kb/guide/mcp-mcp-hermes) Use these checks to troubleshoot Composio MCP connection failures in Hermes / Nous Hermes Agent. ## Production MCP API paths and direct transport tests [#production-mcp-api-paths-and-direct-transport-tests] Use HTTPS and the full production API path: ```text https://backend.composio.dev/api/v3.1/mcp/servers https://backend.composio.dev/api/v3.1/mcp/ ``` Pass the Project API key in `x-api-key`. Avoid an HTTP URL, staging hosts, or a trailing slash on `/servers`, which can produce redirects. For a no-auth server, still pass `auth_config_ids: []` explicitly with `no_auth_apps`. When testing the returned MCP transport directly, include the Project API key, either `user_id` or `connected_account_id`, and `Accept: application/json, text/event-stream`. A redirect from the returned URL to the current Streamable HTTP endpoint is expected when the client follows it. ## Auth configs are project-scoped [#auth-configs-are-project-scoped] A hosted For You/consumer MCP session cannot reuse a custom auth config created in a separate Platform developer project. The session resolves configs only in its own project. For a customer-created Platform Tool Router session, bind a same-project config with its real `ac_*` ID. A display name is not the auth-config ID, and cross-project binding is unsupported. --- # Tool Router Files (/kb/guide/mcp-tool-router-files) ## Session paths are not `FileUploadable` storage keys [#session-paths-are-not-fileuploadable-storage-keys] Tool Router session files and toolkit `FileUploadable` inputs are different abstractions. Do not pass `/workspace/output/...`, `/mnt/files/...`, a local machine path, or an old/foreign `file_...` handle directly as `s3key`. When workbench/meta tools are available: * For a file already under `/mnt/files`, use `get_mount_file_s3_key("file.ext")`. * For another sandbox path, use `upload_local_file("/path/to/file.ext")`. * Pass the returned key to the toolkit action as `{ "name": "file.ext", "mimetype": "...", "s3key": "" }`. In SDK/API flows, upload or stage the file first and pass the fresh returned file object. If an action reports `Failed to download file with s3key ... storage returned HTTP 404`, it failed while resolving the Composio-staged file, before the provider received it. Re-stage the file and retry with the fresh object. --- # Tool Router Sessions (/kb/guide/mcp-tool-router-sessions) ## Create Tool Router sessions through the SDK or API [#create-tool-router-sessions-through-the-sdk-or-api] There is no normal dashboard toggle required to enable Tool Router. Create a session through the SDK or the REST API. * [Quickstart](https://docs.composio.dev/docs/quickstart) * [Configuring sessions](https://docs.composio.dev/docs/configuring-sessions) * [Create a Tool Router session API](https://docs.composio.dev/reference/api-reference/tool-router/postToolRouterSession) If you receive an actual 403 or an error saying Tool Router is not enabled for the account, do not keep repeating the setup steps. Contact Composio support for account-level checking and include the exact error body plus the request or code snippet. ## Session lifetime and deletion [#session-lifetime-and-deletion] Tool Router sessions are long-lived records and do not currently have a time-based expiration. This is separate from temporary workbench files, live sandbox retention, and short response-cache lifetimes. Reuse an existing TypeScript session with `composio.use(sessionId)`. Delete a session either from the instance or by ID: ```text await session.delete(); await composio.sessions.delete(sessionId); ``` Deletion takes effect immediately. A deleted, missing, or inaccessible session returns 404 when retrieved; deleting a session does not delete its users, auth configs, or connected accounts. ## Select among multiple accounts with an alias or account ID [#select-among-multiple-accounts-with-an-alias-or-account-id] When a toolkit has multiple connected accounts, assign clear aliases such as `work`, `personal`, or `primary`, then pass the alias as the execution `account`. Without an alias, use the generated account ID returned by connection discovery. Do not rely on fuzzy phrases such as “office email” unless a matching alias exists. If explicit account selection is disabled and no `account` is supplied, the session can fall back to its first/default account. ## The session user must match the connected-account user [#the-session-user-must-match-the-connected-account-user] An account can be active in the dashboard but unavailable to Tool Router when the session uses a different `user_id`. Private accounts resolve for their owning user; explicitly shared or pinned accounts follow the session configuration. Create the session and connection with the same stable user ID. If a particular account must be used, pass its allowed connected-account override in the session configuration. ## Connected-account selection is live unless pinned [#connected-account-selection-is-live-unless-pinned] When `connectedAccounts` is omitted, Tool Router resolves currently active accounts for the session user at execution time, including accounts connected after session creation. When `connectedAccounts` is supplied, it is an exact toolkit override and Tool Router does not fall back to another active account for that toolkit. Adding another account later does not change an explicit pin. Update or recreate the session when the pinned account should change; omit the override when you want live account discovery. ## Toolkit allowlists are enforced before connection lookup [#toolkit-allowlists-are-enforced-before-connection-lookup] When a session has a non-empty `toolkits.enabled` list, every other toolkit is blocked. A `toolkits.disabled` list does the inverse: listed toolkits are blocked while the rest remain eligible. This restriction is checked before auth configs and connected accounts. If Tool Router reports `[Session Restriction] Toolkit '' is not allowed`, update or recreate the session's toolkit configuration first. Only then debug whether that toolkit has an auth config and connection. ## A fresh task context is a new session runtime, not model memory [#a-fresh-task-context-is-a-new-session-runtime-not-model-memory] Every `create()` call returns a new session ID. A session scopes the user, toolkit and tool access, auth and account selection, and session runtime resources such as sandbox files. It is not the model's conversation memory. Reuse a stored session with `composio.use(sessionId)` when a conversation or workflow should retain the same session configuration and runtime context. Create a new session for a different user or materially different setup. A new session for the same user can still resolve that user's eligible connected accounts, but it does not inherit the old session's sandbox state. ## Auth links create project- and user-scoped connected accounts [#auth-links-create-project--and-user-scoped-connected-accounts] `session.authorize()` and `COMPOSIO_MANAGE_CONNECTIONS` create a Connect Link for the session user and selected auth config. After authentication, the connected account belongs to that project/user rather than only to the session that produced the link. Later unpinned sessions for the same stable user can resolve it; an explicit connected-account pin remains unchanged until the session is updated or recreated. ## Toolkit filters do not preload every matching tool [#toolkit-filters-do-not-preload-every-matching-tool] By default, a session exposes meta tools that discover and load app tools at runtime. Enabling a toolkit limits what the session can discover and execute; it does not put every tool from that toolkit into the initial schema set. Use an explicit `preload.tools` list when the agent must receive known tools directly. Use the direct-tools preset or `preload.tools = "all"` only with a narrow positive filter; broad preload sets are capped and increase agent context. ## SDK custom tools and Custom MCP toolkits have different runtimes [#sdk-custom-tools-and-custom-mcp-toolkits-have-different-runtimes] An SDK-defined custom tool runs inside the customer's application process. Its function body is not uploaded into Composio and is not automatically callable from a remote session MCP URL or Remote Workbench. To expose customer-owned functionality remotely, host it as an MCP server and register it as a Custom MCP toolkit. The resulting remote tools remain subject to the session's toolkit and connection restrictions. ## Enhanced Control requires client support for MCP elicitation [#enhanced-control-requires-client-support-for-mcp-elicitation] For You's Enhanced Control approval flow relies on MCP elicitation. It works only with clients that advertise and implement that capability. If a client does not support elicitation, use a supported client, set an applicable **Always Allow** policy, or disable Enhanced Control under **For You → Settings → General** and reconnect the client. ## Pin the intended auth config when a toolkit has multiple auth schemes [#pin-the-intended-auth-config-when-a-toolkit-has-multiple-auth-schemes] Tool Router first uses the auth config explicitly mapped in the session. When the toolkit supports multiple schemes, map the intended `ac_...` ID rather than depending on automatic selection. The selected config must belong to the same project and be enabled for Tool Router. An explicit connected-account override is an exact toolkit selection and does not fall back to another active account. --- # Compliance, Data Retention, and Model Training (/kb/guide/platform-compliance-data-handling) ## Canonical public sources [#canonical-public-sources] * The [security overview](https://docs.composio.dev/docs/security/overview) describes Composio's security controls, including organization and project isolation, encryption for credentials and keys, TLS in transit, token redaction, and webhook signing. * The [data-retention documentation](https://docs.composio.dev/docs/security/data-retention) explains tool-call log retention, per-project log-storage controls, returned-file URL lifetime, and where data flows during execution. * The [Composio Trust Center](https://trust.composio.dev) provides current compliance reports and sub-processor information. ## Zero data retention and no-training requirements [#zero-data-retention-and-no-training-requirements] Standard plans do not guarantee end-to-end zero data retention or zero training. The per-project **Don't store data** setting reduces what Composio stores, but it does not govern data retained or processed by third-party providers. Customers who require contractual zero-data-retention, no-training, DPA, or security-review terms should use the Enterprise track so the requirements can be scoped explicitly. ## Model training [#model-training] Do not infer a blanket no-training guarantee. Features that use third-party providers are also governed by those providers' terms. For an end-to-end contractual no-training requirement, use the Enterprise track. ## FedRAMP [#fedramp] Composio is not FedRAMP authorized. ## Third-party providers [#third-party-providers] Some toolkit executions and browser automation rely on third-party providers or sub-processors. Data can flow to those providers during execution, and their data and training terms can differ. Use the Trust Center and data-retention documentation for current public details. --- # Connected Accounts (/kb/guide/platform-connected-accounts) Use this for Composio connected-account status, refresh, and identity debugging. ## Prefer a new auth link session when a user must reconnect [#prefer-a-new-auth-link-session-when-a-user-must-reconnect] Create a new auth link session when a user must authenticate again. Redirect the user to the returned hosted link and wait for the resulting connected account to become active. The older `POST /connected_accounts/{nanoid}/refresh` re-initiation endpoint is deprecated; it did not perform Composio's internal background token refresh. If the user completes that auth flow successfully, the connected account can return to `ACTIVE`. Example response: ```text This starts a new authentication flow. For OAuth connections, the user must open the hosted link and complete provider consent. Once the OAuth flow succeeds, use the newly active connected account. ``` ## Same user ID does not prove same upstream account [#same-user-id-does-not-prove-same-upstream-account] Do not assume multiple connected accounts under the same `clientUniqueUserId` are duplicates of the same upstream account. A single Composio user ID can legitimately connect personal, work, and business accounts. If the root-cause hypothesis depends on repeated reconnects to the same upstream Google/Microsoft/etc. account, verify the upstream identity first. Use a safe profile/current-user action for each connected account, customer-provided labels, or another non-sensitive identity signal. ## Hosted connect links expire after 10 minutes [#hosted-connect-links-expire-after-10-minutes] A hosted connect link/session is short-lived. If the initial authentication flow is not completed within 10 minutes, the link can show wording such as “We couldn't verify the session associated with the link” or “Validation error while processing request.” The dashboard may briefly continue to show the connection as initializing. Generate a fresh connect link for the same user and open it immediately. If the new link also fails immediately, contact Composio support with its generation timestamp and the exact error. Do not keep retrying an older link. ## Connection status describes a lifecycle, not credential validity [#connection-status-describes-a-lifecycle-not-credential-validity] * `INITIALIZING`: the connection row and hosted flow were created. * `INITIATED`: the user opened or advanced the authentication flow. * `ACTIVE`: the connection flow completed and its credential data was stored. * `EXPIRED`: the flow timed out or the connection can no longer refresh/use its authorization. Read `statusReason` to distinguish those cases. `Connection initiation did not complete within 10 minutes` means the original flow timed out; it is not a background token-refresh failure. Generate a fresh link and wait for `ACTIVE` before treating its connected-account ID as usable. ## OAuth refresh failures have multiple causes [#oauth-refresh-failures-have-multiple-causes] An OAuth connection may expire when the provider rejects its refresh token, the user or admin revokes the app, provider security policy invalidates the grant, a rotating-token chain is interrupted, or customer-owned OAuth credentials change. Reconnecting obtains a new grant. If connections repeatedly expire across users, contact Composio support with redacted connection IDs and timestamps instead of repeatedly reconnecting. ## Provider tokens are redacted from connected-account responses [#provider-tokens-are-redacted-from-connected-account-responses] Connected-account APIs do not return raw access or refresh tokens. Use Composio tool execution or [Proxy Execute](https://docs.composio.dev/docs/proxy-execute) when a workflow needs to call a provider API through an existing connection. Do not build a workflow that depends on reading provider tokens from connected- account data. ## Revoke provider credentials before removing a connection when required [#revoke-provider-credentials-before-removing-a-connection-when-required] Use the connected-account revoke operation when the toolkit supports programmatic provider revocation. When provider-side revocation is unavailable, remove Composio's access in the provider's connected-app settings or rotate the API key in the provider dashboard. Never send access tokens, refresh tokens, API keys, or private-key material to Composio support. --- # Custom Connection Data Field Names (/kb/guide/platform-custom-connection-data-fields) Use this when a customer creates a custom/API-key style connected account and tool execution fails with `No authentication provided`, 401/403 provider errors, or a provider-specific auth error even though the credential itself works directly against the upstream API. ## Field names are toolkit-specific [#field-names-are-toolkit-specific] Do not assume every API-key or bearer-token toolkit accepts `custom_connection_data.val.api_key`. The required field name is toolkit-specific. For example, this shape is incorrect for Crowdin: ```json { "val": { "api_key": "" } } ``` Crowdin expected: ```json { "val": { "bearer_token": "" } } ``` To verify the required field names, inspect toolkit metadata: ```bash curl --location 'https://backend.composio.dev/api/v3.1/toolkits/' \ --header 'x-api-key: ' ``` Look under: ```text auth_config_details[].fields.connected_account_initiation.required ``` If the mismatch continues, share a request ID or log ID with Composio support. If no request ID is available, share how `custom_connection_data` is being constructed, with secrets removed. Example response: ```text Could you share the `custom_connection_data` shape you're sending, with the secret value removed? The field name is toolkit-specific. For example, some toolkits expect `bearer_token` rather than `api_key`. We can verify the required field from the toolkit metadata and make sure the credential is landing in the right field. ``` --- # File Download Storage and Expiry (/kb/guide/platform-file-storage) ## Composio file URLs are short-lived staged downloads [#composio-file-urls-are-short-lived-staged-downloads] When a hosted tool returns a file URL such as `data.file.s3url`, Composio normally stages the bytes in Composio-managed object storage and returns a signed download URL rather than the provider's original URL. The default signed-URL lifetime is one hour and can be configured for a project through its File TTL setting. Staged files are cleaned up after 24 hours. URL expiry and file cleanup are separate: rerun the tool or download the file again to obtain a fresh URL. There is no single customer-facing maximum that applies to every tool. Provider limits, the action implementation, runtime memory, and timeouts can impose lower limits, so check the exact action before quoting a hard cap. --- # Google OAuth setup and consent (/kb/guide/platform-google-oauth) ## An unapproved Google OAuth scope can block consent [#an-unapproved-google-oauth-scope-can-block-consent] Google can block sign-in when an OAuth app requests a sensitive or restricted scope that is not approved for that app. Use the scopes already available on the selected Composio auth config, or create a customer-owned Google OAuth app and complete Google's required verification before requesting additional scopes. After changing scopes, create a fresh connection so the user grants the new scope set. Google's current verification requirements are documented in its [OAuth 2.0 policies](https://developers.google.com/identity/protocols/oauth2/policies) and [sensitive-scope verification guide](https://developers.google.com/identity/protocols/oauth2/production-readiness/sensitive-scope-verification). ## A customer-owned OAuth app controls the provider consent-screen brand [#a-customer-owned-oauth-app-controls-the-provider-consent-screen-brand] Use a customer-owned Google OAuth app when the Google consent screen should show the customer's app name and branding. To avoid showing a Composio domain in the redirect path as well, route the callback through the customer's domain as described in [white-labeling authentication](https://docs.composio.dev/docs/white-labeling-authentication#routing-the-callback-through-your-domain). The OAuth app's authorized redirect URI must still match the callback URI shown by Composio. Provider consent-screen branding and the URL to which the customer's application sends a user after authentication are separate settings. --- # Platform Health Endpoints (/kb/guide/platform-health-endpoints) Use this only for Composio on-prem / self-hosted customers who ask whether they can monitor their Composio instance in real time. These endpoints are not general public-cloud customer endpoints. Requests must include the Composio admin token header: ```http x-composio-admin-token: ``` ## Apollo [#apollo] Basic liveness: ```bash curl -i "$COMPOSIO_BASE_URL/api/healthz" \ -H "x-composio-admin-token: $COMPOSIO_ADMIN_TOKEN" ``` Success: ```json { "status": "ok" } ``` This only confirms that Apollo can serve the request. It does not check downstream dependencies. Deep dependency health: ```bash curl -sS "$COMPOSIO_BASE_URL/api/deep_healthz" \ -H "x-composio-admin-token: $COMPOSIO_ADMIN_TOKEN" | jq ``` Example: Apollo deep health checks: * `postgres`: `SELECT 1` through Prisma. * `redis`: Redis `PING`. * `thermos`: generated Thermos client `getHealthcheck()`, which calls Thermos `GET /api`. * active object storage backend: response key is either `s3` or `azure_blob_storage`; Apollo writes a zero-byte probe object and deletes it best-effort. Important: Apollo deep health returns HTTP `200` for GET requests even when one or more dependencies are unreachable. Monitors should inspect `data..reachable`, not just HTTP status. ## Thermos [#thermos] Basic liveness: ```bash curl -i "$THERMOS_BASE_URL/api" \ -H "x-composio-admin-token: $COMPOSIO_ADMIN_TOKEN" ``` Example: ```json { "status": "ok", "time": "2026-06-19T05:37:25Z" } ``` Deep dependency health: ```bash curl -sS "$THERMOS_BASE_URL/api/health/deep" \ -H "x-composio-admin-token: $COMPOSIO_ADMIN_TOKEN" | jq ``` Example: Required services are `database`, `toolkit_registry_database`, and `temporal`. Thermos status behavior: * `healthy`: required services are not in `error`. * `unhealthy`: required service `database`, `toolkit_registry_database`, or `temporal` is in `error`. Thermos returns HTTP `503` only when overall status is `unhealthy`; otherwise it returns HTTP `200`. --- # Microsoft OAuth scopes and tenant consent (/kb/guide/platform-microsoft-oauth) ## Request `offline_access` when a delegated connection needs refresh tokens [#request-offline_access-when-a-delegated-connection-needs-refresh-tokens] Microsoft's v2 OAuth endpoint requires an explicit `offline_access` request to return refresh tokens. Composio's standard Microsoft delegated OAuth scope sets include it. For a customer-owned Microsoft app, include `offline_access` in the app and Composio auth-config scopes before creating a new connection. Microsoft documents this behavior in [Scopes and permissions in the Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc#the-offline_access-scope). ## Some Microsoft tenant policies and permissions require administrator consent [#some-microsoft-tenant-policies-and-permissions-require-administrator-consent] A work or school account can show **Needs Admin Approval** or **Admin approval required** when the tenant prevents users from approving the app or when the requested permission is administrator-restricted. A tenant administrator must approve the selected Composio-managed app or the customer's own app and its requested permissions. The affected user should then start a fresh connection. Adding a permission to an Entra app registration does not itself grant tenant consent. Microsoft explains the distinction in its [permissions and consent overview](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview). This guidance applies across Microsoft toolkits that use delegated Microsoft OAuth, including Outlook, Microsoft Teams, OneDrive, OneNote, Excel, Power BI, and Dynamics 365. SharePoint REST and app-only/S2S flows may also require resource-specific permissions and administrator consent. --- # Platform Pagination (/kb/guide/platform-pagination) ## Pagination limits are endpoint-specific [#pagination-limits-are-endpoint-specific] Composio does not have one global page-size limit. Resource lists, catalogs, Tool Router, logs, and billing endpoints can define different limits, while toolkit actions also inherit provider-specific rules. Check the exact endpoint schema and live behavior before quoting a maximum. ## Auth-config list pages return at most 50 items [#auth-config-list-pages-return-at-most-50-items] `GET /api/v3/auth_configs` and `GET /api/v3.1/auth_configs` currently return at most 50 auth configs per page. Read `next_cursor` from each response and pass it as `cursor` until it is empty. Some generated descriptions may advertise a larger limit; the deployed endpoint still clamps the page to 50. Treat that documentation/runtime mismatch as a product issue, not as a reason to skip cursor pagination. --- # Move a Composio Integration from Prototype to Production (/kb/guide/platform-production-readiness) ## Replace example user IDs with stable application user IDs [#replace-example-user-ids-with-stable-application-user-ids] Create sessions and connected accounts with a stable identifier from the application database, such as a UUID or primary key. Do not use an email address that can change, and never use `default` in production. Composio uses the user ID to isolate connections and tool calls, so each application user must resolve to the same Composio user ID across sessions. * [Authentication and user IDs](https://docs.composio.dev/docs/authentication) * [How Composio sessions work](https://docs.composio.dev/docs/how-composio-works) ## Isolate environments with separate Composio projects when needed [#isolate-environments-with-separate-composio-projects-when-needed] A Composio project scopes its API keys, connected accounts, auth configs, and webhooks. Use separate projects for development, staging, and production when those resources must not overlap. Use the API key for the intended project in each deployment, and create environment-specific auth configs when the OAuth apps, scopes, or provider credentials differ. * [Composio glossary: Project](https://docs.composio.dev/reference/glossary#project) * [Configure authentication](https://docs.composio.dev/docs/tools-direct/authenticating-tools) ## Switch from managed auth only when production requirements call for it [#switch-from-managed-auth-only-when-production-requirements-call-for-it] Composio managed auth is suitable for development, internal tools, and early prototypes. Create a custom auth config when users must see the application's own OAuth brand, the integration needs custom scopes or a dedicated provider quota, polling requirements differ, or the provider uses a custom instance. Pass the resulting auth config ID to the session; creating the config alone does not make the session use it. * [Managed vs custom auth](https://docs.composio.dev/docs/authentication/custom-app-vs-managed-app) * [Controlling OAuth scopes](https://docs.composio.dev/docs/authentication/controlling-scopes) ## Restrict the production session to the capabilities the agent needs [#restrict-the-production-session-to-the-capabilities-the-agent-needs] Set toolkit, tool, and behavior-tag filters when creating the session. For a sensitive or deterministic workflow, prefer an explicit allowlist of exact tool slugs. For a broader read-only agent, filter on `readOnlyHint` and disable `destructiveHint`, then inspect the resulting tool set before rollout. * [Configure session tool access](https://docs.composio.dev/docs/configuring-sessions) * [Create read-only and restricted sessions](/kb/guide/platform-session-tool-policies) ## Reuse a stored session until the user or configuration changes [#reuse-a-stored-session-until-the-user-or-configuration-changes] Store the session ID and restore it with `composio.use(session_id)` instead of creating a new session for every turn. Create a new session for a different user or a materially different setup, such as a new tool policy or auth-config mapping. A session preserves its scoped runtime state, but it is not the language model's conversation memory. * [Reuse a session](https://docs.composio.dev/docs/how-composio-works#how-sessions-behave) ## Test production trigger handling through the real webhook path [#test-production-trigger-handling-through-the-real-webhook-path] The local `subscribe()` stream is useful for inspecting events, but it bypasses the production webhook handler and signature verification. Before rollout, forward events to the real local handler or use a tunnel, verify the signed payload with `parse()`, and then register the production HTTPS webhook URL for the production project. * [Receive trigger events locally and in production](https://docs.composio.dev/docs/setting-up-triggers/subscribing-to-events) --- # Project API Key Permissions (/kb/guide/platform-project-api-key-permissions) ## Proxy Execute requires an explicitly allowed Project API key [#proxy-execute-requires-an-explicitly-allowed-project-api-key] Create a scoped Project API key in the Dashboard and enable **Proxy Execute** during key creation before calling the v3.1 Proxy Execute API. If a request is denied, verify the key's scope before debugging the provider connection. Use a fresh request ID from the correctly scoped key when contacting Composio support is still necessary. ## Tool Router session creation requires Sessions write access [#tool-router-session-creation-requires-sessions-write-access] For scoped Project API keys, creating a session through `composio.sessions.create(...)` or `POST /api/v3.1/tool_router/session` requires the Sessions permission with write or read/write access. A key can successfully call `GET /api/v3.1/toolkits` with Toolkits read access and still be unable to create a session. The SDK can surface a scoped-permission denial as a generic 401 `Invalid API key`. Create a new Project API key with Sessions set to Read and write, or use an appropriate full-access Project API key, then retry session creation. ## Tool execution requires Tool execution write access [#tool-execution-requires-tool-execution-write-access] For a scoped Project API key, `composio.tools.execute()` and the tool-execute API require Tool execution set to Write or Read and write. A key without that permission can surface a generic 401 `Invalid API key` even when the key exists and is active. Create a correctly scoped Project API key or use an appropriate full-access Project API key, then retry. The current API may return a generic permission error, so diagnose this behavior from the key's permissions. --- # Platform API Rate Limits (/kb/guide/platform-rate-limits) ## Organization API limits and 429 handling [#organization-api-limits-and-429-handling] Composio applies a shared API budget per organization across authenticated endpoints. Current published limits are Starter and Hobby: 2,000 requests per minute; Growth: 10,000 per minute; Enterprise: custom. Check the [current rate-limit documentation](https://docs.composio.dev/reference/rate-limits) before quoting a plan limit, and do not describe Enterprise as unlimited. Rate-limit responses include remaining/window information, and a 429 includes `Retry-After`. Honor `Retry-After` before retrying. Provider quotas such as Google API limits are separate and can throttle a tool even when the Composio organization has capacity. If an upgraded organization still sees its old 2,000-per-minute ceiling, share the error time and response rate-limit headers with support. --- # Support Routing (/kb/guide/platform-routing) Use these routes when a request is not primarily a support or debugging issue. ## Hiring [#hiring] Direct hiring inquiries to Composio's public careers channel. ## DPA, enterprise data handling, and compliance [#dpa-enterprise-data-handling-and-compliance] Contractual data-handling requirements such as zero data retention, no-training terms, a DPA, or a security review belong in the Composio Enterprise track. See [Compliance, Data Retention, and Model Training](/kb/guide/platform-compliance-data-handling) for the approved public guidance. ## Tool or toolkit feature requests [#tool-or-toolkit-feature-requests] Submit requests through the [Composio request board](https://request.composio.dev). --- # Self-hosted Helm (/kb/guide/platform-self-hosted-helm) Use these checks to troubleshoot Composio self-hosted or on-prem Helm deployments. ## Apollo S3 with IRSA / ServiceAccount credentials needs no static S3 secret keys [#apollo-s3-with-irsa--serviceaccount-credentials-needs-no-static-s3-secret-keys] For IRSA / ServiceAccount-based S3 access, `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` should not be populated in the Apollo container. These secret keys are optional. If the keys are removed from `composio-composio-secrets`, Apollo can fall back to the configured pod ServiceAccount / AWS SDK credential chain. Do not set placeholder S3 credential values for IRSA deployments. Values such as `dummy-value` or a literal string `null` are treated as credentials and can make S3 pre-signed URLs fail with provider errors such as: ```text InvalidAccessKeyId: The AWS Access Key Id you provided does not exist in our records. InvalidToken: The provided token is malformed or otherwise invalid. ``` For AWS IRSA, configure the Apollo ServiceAccount annotation and object storage backend, then leave static S3 credential secret keys absent: ```yaml apollo: serviceAccount: enabled: true name: "composio-apollo" annotations: eks.amazonaws.com/role-arn: "arn:aws:iam:::role/" objectStorage: backend: "s3" ``` Debug checks: ```bash kubectl exec -n composio deploy/composio-apollo -- env | grep -E "^S3_|^AWS_" kubectl logs -n composio deploy/composio-apollo --tail=200 ``` If using pod/container credentials, the Helm storage doc says the Kubernetes secret credential section can be skipped. Verify that `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` are not present with dummy/static values in Apollo's runtime environment. Example response: ```text This looks like Apollo is still receiving static S3 credential env vars, so the AWS SDK is using those instead of falling back to the ServiceAccount/IRSA credentials. For IRSA, `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` should be absent from the Apollo container. Those secrets are optional; if you remove those keys from `composio-composio-secrets`, Apollo should use the configured ServiceAccount. Placeholder values like `dummy-value` or literal `null` are treated as real credentials and can cause S3 signing errors such as `InvalidAccessKeyId` or `InvalidToken`. The immediate fix is to remove `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` from `composio-composio-secrets`, then confirm the Apollo pod environment no longer includes those values. ``` ## Disable social login explicitly for self-hosted deployments [#disable-social-login-explicitly-for-self-hosted-deployments] `NEXT_PUBLIC_DISABLE_SOCIAL_LOGIN` controls whether social login buttons such as Google/GitHub appear on the frontend login page. For self-hosted customers that should hide social login, set the Helm values override as a string: ```yaml apollo: nextPublicDisableSocialLogin: "true" ``` This makes the generated Apollo ConfigMap render an explicit value instead of an empty/null value and should remove repeated ArgoCD diffs immediately. Customers can apply the override directly to unblock the deployment. Example response: ````text `NEXT_PUBLIC_DISABLE_SOCIAL_LOGIN` controls whether the Google/GitHub social login buttons show up on the frontend login page. For your setup, I'd set it explicitly to `"true"` in the Helm values override: ```yaml apollo: nextPublicDisableSocialLogin: "true" ``` This should make the generated Apollo ConfigMap render the explicit value and stop ArgoCD from diffing null vs empty string immediately. The override above can be applied directly; no release-specific promise is required. ```` --- # Create Read-Only and Restricted Composio Sessions (/kb/guide/platform-session-tool-policies) ## Filter a broad read-only session with behavior tags [#filter-a-broad-read-only-session-with-behavior-tags] Use session-level behavior tags when the agent may discover tools across multiple toolkits but should only receive tools marked as read-only. The same filters are enforced when the session executes tools. **Python** ```python session = composio.sessions.create( user_id="user_123", tags={ "enable": ["readOnlyHint"], "disable": ["destructiveHint"], }, ) ``` **TypeScript** ```text const session = await composio.create("user_123", { tags: { enable: ["readOnlyHint"], disable: ["destructiveHint"], }, }); ``` Behavior tags describe tool behavior. Inspect the tools the session exposes before rollout, especially when a workflow handles sensitive data. * [Filter tools by tags](https://docs.composio.dev/docs/configuring-sessions#filtering-tools-by-tags) ## Use exact tool allowlists for the narrowest policy [#use-exact-tool-allowlists-for-the-narrowest-policy] For a workflow with a known set of operations, allow only the exact tool slugs it requires. An allowlist avoids admitting a newly added tool merely because it shares a toolkit or behavior tag. **Python** ```python session = composio.sessions.create( user_id="user_123", tools={ "gmail": {"enable": ["GMAIL_FETCH_EMAILS"]}, "github": {"enable": ["GITHUB_GET_AN_ISSUE"]}, }, ) ``` **TypeScript** ```text const session = await composio.create("user_123", { tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS"] }, github: { enable: ["GITHUB_GET_AN_ISSUE"] }, }, }); ``` * [Enable and disable specific tools](https://docs.composio.dev/docs/configuring-sessions#enabling-or-disabling-specific-tools) ## Combine provider scopes with session tool restrictions [#combine-provider-scopes-with-session-tool-restrictions] OAuth scopes control what the provider grants to a connected account. Session filters control which Composio tools the agent can discover and execute. Use both layers for least privilege: request only the provider scopes the use case needs, then restrict the session to the intended tools. Changing an auth config's scopes affects new connections only. Existing users keep their prior grants until they reconnect. Pass the intended auth config ID to the session, keyed by toolkit, or the session will not request those scopes. * [Control OAuth scopes](https://docs.composio.dev/docs/authentication/controlling-scopes) * [Select an auth config in a session](https://docs.composio.dev/docs/authentication/custom-app-vs-managed-app#create-a-custom-auth-config) ## Apply toolkit-specific exceptions without widening every toolkit [#apply-toolkit-specific-exceptions-without-widening-every-toolkit] Set a global tag policy and override it only for a named toolkit. This is safer than relaxing the global policy for the entire session. **Python** ```python session = composio.sessions.create( user_id="user_123", tags=["readOnlyHint"], tools={ "github": {"tags": {"disable": ["destructiveHint"]}}, "gmail": {"tags": ["readOnlyHint"]}, }, ) ``` **TypeScript** ```text const session = await composio.create("user_123", { tags: ["readOnlyHint"], tools: { github: { tags: { disable: ["destructiveHint"] } }, gmail: { tags: ["readOnlyHint"] }, }, }); ``` * [Toolkit-specific tag filters](https://docs.composio.dev/docs/configuring-sessions#filtering-tools-by-tags) ## Disable the session sandbox when the workflow does not need code execution [#disable-the-session-sandbox-when-the-workflow-does-not-need-code-execution] Session tool filters govern app tools. Sessions also include remote sandbox tools by default. Disable the sandbox for a tightly constrained workflow that does not need Python, shell, file processing, or remote workbench execution. **Python** ```python session = composio.sessions.create( user_id="user_123", tags=["readOnlyHint"], sandbox={"enable": False}, ) ``` **TypeScript** ```text const session = await composio.create("user_123", { tags: ["readOnlyHint"], sandbox: { enable: false }, }); ``` * [Disable the session sandbox](https://docs.composio.dev/docs/configuring-sessions#disabling-the-sandbox) --- # Tool Router Workbench Retention (/kb/guide/platform-tool-router-workbench) Use this when customers ask whether Tool Router workbench state persists forever, or whether personal data used in the remote workbench is retained indefinitely. ## A “fresh” client label can still reuse the same sandbox [#a-fresh-client-label-can-still-reuse-the-same-sandbox] Workbench and sandbox reuse follows the actual Tool Router session, not an arbitrary client-side session label. Reusing the same `trs_*` session or MCP URL can reuse the same cached workbench and sandbox. Create a new Tool Router session and use its newly returned MCP URL when a workflow requires an independent sandbox rather than reused session state. --- # Triggers (/kb/guide/platform-triggers) ## Find every toolkit that currently supports triggers [#find-every-toolkit-that-currently-supports-triggers] Do not rely on a dashboard count as the complete trigger catalog because availability changes and list views can be partial. * Call `GET /api/v3.1/triggers_types` to list trigger types and their parent toolkits. Use `toolkit_slugs` to narrow the result when needed. * Or call `GET /api/v3.1/toolkits` and select toolkits whose `triggers_count` is greater than zero. * Follow pagination through every result page before calculating a total or claiming the list is complete. * Each trigger type declares its required configuration and may be webhook/event-driven or polling-based. References: [trigger types API](https://docs.composio.dev/reference/api-reference/triggers/getTriggersTypes), [toolkits API](https://docs.composio.dev/reference/api-reference/toolkits/getToolkits), and [creating triggers](https://docs.composio.dev/docs/setting-up-triggers/creating-triggers). ## Trigger webhook delivery is at-least-once [#trigger-webhook-delivery-is-at-least-once] A receiver can occasionally see the same trigger webhook more than once, including the same `log_id` or provider event/message ID, when an outbound delivery attempt is retried. This does not necessarily mean Composio ingested the provider event twice. Webhook handlers should be idempotent and deduplicate on a stable identifier such as `log_id`, the provider message/event ID, or the webhook event ID. If duplicates continue beyond normal retry behavior, contact Composio support with the relevant IDs and receipt timestamps. --- # Tool Execution Retries (/kb/guide/sdk-tool-execution-retries) ## Current SDKs do not automatically retry non-idempotent tool executions [#current-sdks-do-not-automatically-retry-non-idempotent-tool-executions] Python SDK 0.16.0 and TypeScript SDK 0.14.0 changed tool execution and Proxy Execute so non-idempotent writes are not automatically retried after timeouts, rate limits, or server errors. Upgrade to at least those versions before diagnosing duplicate sends or writes as current SDK retry behavior. An ambiguous client timeout still does not prove that the provider-side action failed. Before manually retrying a send, create, update, or delete action, inspect the execution log or provider state to determine whether the first attempt completed. If duplicates persist on a current SDK, collect the SDK version, execution log IDs, and timestamps for support. Suggested guidance: ```text Current Composio SDKs do not automatically retry non-idempotent tool executions. A timeout can still be ambiguous, so check the execution log or provider state before manually retrying an action that may have completed. ``` --- # TypeScript Tool Schema Definitions (/kb/guide/sdk-tool-schemas) ## Upgrade when `$ref` is present but root `$defs` is missing [#upgrade-when-ref-is-present-but-root-defs-is-missing] Older `@composio/core` releases through 0.11.0 could preserve a nested `$ref` while stripping the root `$defs` or `definitions` block from raw tool schemas. Downstream schema parsers then see a dangling reference. The shared fix shipped in `@composio/core` 0.12.0. Upgrade core to 0.12.0 or later and use a compatible provider-package version. That release line is ESM-only and requires Node.js 22.22.3 or later, so confirm runtime and provider compatibility before upgrading. After upgrading, fetch the exact tool again and verify every internal `$ref` has a matching root definition. --- # Ahrefs (/kb/guide/toolkits-ahrefs) ## Ahrefs actions must call api.ahrefs.com, not ahrefs.com [#ahrefs-actions-must-call-apiahrefscom-not-ahrefscom] Ahrefs API calls should use the API host `https://api.ahrefs.com/v3`. If Ahrefs actions or connection checks are hitting `https://ahrefs.com/v3` and returning 404 HTML, treat it as a connector base-URL configuration problem rather than an API-key or request-payload issue. Confirm the failing request is using api.ahrefs.com; if it is not, contact Composio support with the redacted request or log ID for connector review. --- # Airtable (/kb/guide/toolkits-airtable) Use this guide to connect Airtable, discover and execute current tools, and configure metadata triggers. ## Connect and authenticate Airtable [#connect-and-authenticate-airtable] **Connect Airtable to Claude through MCP.** Airtable can be connected to Claude through Composio MCP. Create or use an MCP server with Airtable tools selected, add the MCP server configuration to Claude, and complete the Airtable account connection from the MCP/connect flow. **Use custom OAuth credentials for additional scopes.** For additional Airtable scopes, use your own Airtable OAuth developer app. Configure the required scopes in Airtable, enable/use custom OAuth credentials in Composio, and create a new integration/auth config with those credentials and scopes. If an existing integration was created before the scope change, create a new one and retry the connection. **Restart connection flows that exceed ten minutes.** The expiry reason "Connection initiation did not complete within 10 minutes" means the user opened or initiated the connection but did not finish the authentication flow within ten minutes. It is a generic connected-account timeout across toolkits, not an Airtable-specific error. Start a fresh connection/initiation link and complete the OAuth flow within the allowed window. ## Discover and execute Airtable tools [#discover-and-execute-airtable-tools] **Increase list limits and use a current toolkit version.** If Airtable tools appear missing, first increase the tools list limit or paginate because the response may contain only the first page. Explicitly request the latest toolkit version when a pinned version lacks a current action. Old names such as `create_multiple_records` and `create_record` were deprecated in favor of current uppercase slugs such as `AIRTABLE_CREATE_RECORDS`. **Batch updates in groups of ten records.** `AIRTABLE_UPDATE_MULTIPLE_RECORDS` can update a maximum of 10 Airtable records at a time. For larger updates, split the records into batches of 10 and execute multiple calls while respecting Airtable's API rate limits. ## Configure Airtable metadata triggers [#configure-airtable-metadata-triggers] **Choose an event from the current trigger catalog.** The current Airtable toolkit exposes triggers for base metadata changes, base schema changes, user profile changes, and view creation, deletion, or metadata changes. Fetch the current trigger catalog before implementation and use the exact returned slug. If the needed event is not in that catalog, submit that Airtable event through the Composio request portal. --- # Apollo (/kb/guide/toolkits-apollo) ## Apollo 403s on search/enrichment endpoints can be key-permission or plan-access gated [#apollo-403s-on-searchenrichment-endpoints-can-be-key-permission-or-plan-access-gated] For Apollo 403 errors on search/enrichment-style endpoints, first confirm whether your Apollo API key has the relevant endpoint enabled or has **Set as master key** turned on. Apollo documents People API Search as requiring a master API key, and Apollo API keys can be created with either individual endpoint access or master-key access. Apollo also gates advanced API access by plan, so a 403 can be Apollo-side endpoint permission, master-key, credit/API-access, or plan gating even when other Apollo tools work. Checks to isolate the cause: * Confirm the Composio credential field is `generic_api_key`. * Run the exact upstream Apollo endpoint directly with the same key and compare the redacted status and response body. * If `APOLLO_GET_AUTH_STATUS` or `APOLLO_VIEW_API_USAGE_STATS` succeeds but search/enrichment endpoints fail, do not say the key is definitely invalid. Phrase it as Apollo endpoint permission / master-key / plan-access gating. * If you contact Composio support, include the failing Composio log ID, upstream endpoint, and whether the Apollo key was created with **Set as master key** or per-endpoint permissions. ## Apollo people enrichment and bulk enrichment can behave differently [#apollo-people-enrichment-and-bulk-enrichment-can-behave-differently] Apollo's single people enrichment and bulk people enrichment APIs do not behave identically. `APOLLO_PEOPLE_ENRICHMENT` and `APOLLO_BULK_PEOPLE_ENRICHMENT` call different upstream Apollo endpoints, and the bulk endpoint may require more complete or different unique person details. If single enrichment works but bulk enrichment does not, compare against Apollo's official bulk people enrichment API behavior before treating it as a Composio response transformation issue. Composio does not intentionally modify the upstream Apollo response. ## Apollo search results may mirror Apollo's official API behavior [#apollo-search-results-may-mirror-apollos-official-api-behavior] When Apollo search returns unexpected results, compare the Composio tool call with the equivalent Apollo official API request using the same query parameters and API key. If Apollo's official endpoint returns the same response, the behavior is upstream from Apollo rather than a Composio transformation. Use the direct Apollo API curl as the baseline for debugging search filters and response differences. --- # Asana (/kb/guide/toolkits-asana) ## Use `ASANA_GET_STORIES_FOR_TASK` and pass the task ID as a string [#use-asana_get_stories_for_task-and-pass-the-task-id-as-a-string] Asana represents task comments as stories. Use `ASANA_GET_STORIES_FOR_TASK` to retrieve the comments and activity for a task, and pass the task ID as a string rather than an integer. For custom toolkit-based tools, set the Asana base URL to `https://app.asana.com/api/1.0` and include the required Authorization header. ## Use the current Asana task triggers [#use-the-current-asana-task-triggers] The current Asana toolkit exposes triggers for task creation, updates, comments, attachments, tags, and moves between sections. Fetch the trigger catalog before implementation and use the exact returned slug, such as `ASANA_TASK_COMMENT_ADDED` or `ASANA_TASK_UPDATED`. --- # Attio (/kb/guide/toolkits-attio) ## Use $contains for partial text matching in ATTIO\_FIND\_RECORD filters [#use-contains-for-partial-text-matching-in-attio_find_record-filters] For partial matching on text attributes in ATTIO\_FIND\_RECORD, structure the filter with the attribute slug mapped to a $contains condition, for example \{"name": \{"$contains": "John"}}. If you receive exact-match behavior instead, verify the specific attribute and filter shape, then try the contains-style filter first. ## Use custom tools when an Attio API object is not built into Composio yet [#use-custom-tools-when-an-attio-api-object-is-not-built-into-composio-yet] If an Attio endpoint is not covered by the built-in toolkit, create a custom tool and request the missing tool through the Composio request portal. Custom tools can use Composio-managed auth, so you do not need to build the entire OAuth/token-storage layer yourself. ## Top-level $ parameter names were fixed for LLM provider compatibility in the latest schema version [#top-level--parameter-names-were-fixed-for-llm-provider-compatibility-in-the-latest-schema-version] For schema failures caused by top-level $-prefixed parameter names, update to the latest tool schema/toolkit version. The root cause was corrected for top-level $ prefixes, and compatibility was verified across OpenAI, Claude, Gemini, and Vercel AI SDK. Nested $ prefixes were accepted by the major providers tested, while broader parameter naming conventions may still need case-specific review. ## Attio toolkit defaults can stay on the base pinned version unless a version is explicitly selected [#attio-toolkit-defaults-can-stay-on-the-base-pinned-version-unless-a-version-is-explicitly-selected] Do not assume Attio calls use the latest toolkit definition automatically. Composio can default to an older base pinned version because latest versions can change. If you need updated Attio tool descriptions or fixes, explicitly set the Attio toolkit version in your SDK or environment and then retest. --- # Browser Tool (/kb/guide/toolkits-browser-tool) ## Browser Tool profiles do not work in Zero Data Retention projects [#browser-tool-profiles-do-not-work-in-zero-data-retention-projects] Browser Tool requires persistent browser profiles to maintain session state, so it is incompatible with projects configured for Zero Data Retention or removal of execution data. Move the Browser Tool usage to a project without ZDR enabled, or change the project's log/data visibility setting from removing execution data to storing/showing all logs where policy allows. The dashboard path is Project Settings / Log storage configuration, and the API setting is `log_visibility_setting: show_all`. --- # Calendly (/kb/guide/toolkits-calendly) ## Use CALENDLY\_POST\_INVITEE instead of deprecated CALENDLY\_CREATE\_EVENT\_INVITEE [#use-calendly_post_invitee-instead-of-deprecated-calendly_create_event_invitee] For Calendly invitee creation flows, prefer `CALENDLY_POST_INVITEE` instead of the legacy `CALENDLY_CREATE_EVENT_INVITEE`. New implementations and migration guidance should point customers to `CALENDLY_POST_INVITEE`. --- # Canva (/kb/guide/toolkits-canva) ## Use Canva autofill jobs when content must be populated into a design [#use-canva-autofill-jobs-when-content-must-be-populated-into-a-design] For Canva workflows that need content inserted into a generated design, do not rely on the create-design endpoint/tool. `CANVA_CREATE_CANVA_DESIGN_WITH_OPTIONAL_ASSET` is deprecated and should be replaced with `CANVA_POST_DESIGNS`, but both the old and new create-design flows create a blank design by default and do not accept arbitrary content in the request. Use `CANVA_INITIATE_CANVA_DESIGN_AUTOFILL_JOB` for the content-population use case, because that flow is built around Canva's autofill capability. --- # Canvas (/kb/guide/toolkits-canvas) Use this guide to configure Canvas authentication and permissions, set up triggers, run Canvas actions, and troubleshoot course or toolkit-version issues. ## Configure Canvas authentication and permissions [#configure-canvas-authentication-and-permissions] **Check action-level scopes when Canvas returns 401 or unauthorized.** Compare the auth configs and verify that the failing Canvas connection has the scope required by the action. `CANVAS_GET_USER_PROFILE` requires `url:GET|/api/v1/users/:user_id/profile`. If scopes are missing, update the auth config settings; newly created connected accounts will get the updated scopes from that point onward. **Match OAuth credentials to the configured Canvas base URL.** For Canvas OAuth, the client ID and client secret must belong to the same Canvas base URL configured on the connection/auth config. A mismatch between the Canvas domain, base URL, and OAuth credentials can cause auth failures even if the credentials are otherwise valid. **Use an administrator for account-level endpoints.** Canvas account-level endpoints require account administrator permissions in Canvas. Use `CANVAS_LIST_MANAGEABLE_ACCOUNTS` to list accounts the connected user can manage, and `CANVAS_GET_SINGLE_ACCOUNT` when the account ID is already known. If you get an authorization error, confirm that the connected Canvas user has account-level admin permissions before treating it as a Composio-side failure. ## Set up Canvas triggers [#set-up-canvas-triggers] **Select courses by their Canvas IDs.** Canvas triggers are available. For a course-based setup flow, first call `CANVAS_LIST_COURSES` or the relevant get-courses action, show the course IDs with their course names to the user, and then redirect the user to the trigger configuration page with the selected course context. **Target users visible to the connected bearer-token user.** Canvas trigger behavior is tied to the user represented by the bearer token on the connected account. A trigger should work for users visible through `CANVAS_GET_ALL_USERS` for the relevant account. The user field cannot be removed entirely because Composio cannot infer every logged-in Canvas user from the provider token without a configured target. **Use a Teacher account for Assignment Graded.** For Canvas Assignment Graded, the trigger can work for Teacher accounts but not Student accounts because of Canvas permission behavior. If the same connected account also has token-expiry symptoms, execute a Canvas action on that connected account to separate permission behavior from connection/auth issues. **Distinguish Canvas and Composio user IDs in payloads.** Canvas trigger payloads now separate the Canvas-side user identifier from Composio's user identifier. Use `canvas_user_id` for the Canvas LMS user and `user_id` for the Composio/project user. This avoids ambiguity when both identifiers are present in the same payload. ## Execute Canvas actions and handle provider behavior [#execute-canvas-actions-and-handle-provider-behavior] **Follow Canvas field descriptions for calendar events.** For `CANVAS_CREATE_CALENDAR_EVENT`, a Canvas user ID can be used where accepted by the Canvas API. Composio keeps Canvas API field names to stay consistent with the provider API, so rely on each field description for accepted values when the field name is ambiguous. **Paginate list and fetch endpoints with `per_page`.** Canvas list endpoints follow Canvas API pagination behavior. Where supported, pass `per_page` to control how many records are returned in a response. If a Canvas action appears capped or returns a smaller page, check whether the relevant tool version supports `per_page` and upgrade if needed. **Use `only_announcements` to request discussion topics and announcements separately.** For Canvas discussion topics, use `only_announcements: false` or omit it when calling the discussion-topic flow. For announcements, use `only_announcements: true`. Canvas cannot return both discussion topics and announcements in one combined call for this case, so make two separate API calls and merge the results client-side if both are needed. **Use unprefixed keys for quiz matching answers.** For Canvas quiz matching question answers, use `comments_html`, `text`, `weight`, `match_left`, and `match_right`. Do not use `answer_comments_html`, `answer_text`, `answer_weight`, `answer_match_left`, or `answer_match_right` for this payload. ## Troubleshoot Canvas courses and toolkit versions [#troubleshoot-canvas-courses-and-toolkit-versions] **Verify the course before diagnosing analytics 404s.** For Canvas course-level participation or analytics actions, first verify the course ID by listing courses or fetching the course by ID with `CANVAS_LIST_COURSES` or `CANVAS_GET_SINGLE_COURSE`. If the course ID is valid but the analytics endpoint still 404s, the Canvas analytics activity endpoint may simply not be available on that Canvas instance. **Upgrade instead of patching older toolkit versions.** Composio cannot patch older toolkit versions in place. If a Canvas behavior or schema fix is released in a newer version, the path is to upgrade the toolkit version. Customers can compare differences between toolkit versions in the dashboard before upgrading. --- # ClickUp (/kb/guide/toolkits-clickup) ## ClickUp supports managed OAuth, custom OAuth, and API-key credentials [#clickup-supports-managed-oauth-custom-oauth-and-api-key-credentials] Use Composio-managed OAuth for the standard connection flow. Use a custom ClickUp OAuth app or API-key credentials when you need greater control. In newer SDK/API flows, use the v3 auth config nano ID (`ac_...`) rather than older v1/v2 integration assumptions. ## ClickUp custom OAuth should use the Composio callback URL registered in the ClickUp app [#clickup-custom-oauth-should-use-the-composio-callback-url-registered-in-the-clickup-app] For ClickUp custom OAuth, make sure the redirect URL in the ClickUp app matches the callback shown by the current Composio auth-config flow. A mismatch between the current auth config and an old callback copied from a legacy SDK example is a common cause of setup failure. ## ClickUp folders and tasks are supported through `CLICKUP_GET_FOLDERS` and `CLICKUP_GET_TASKS` [#clickup-folders-and-tasks-are-supported-through-clickup_get_folders-and-clickup_get_tasks] For ClickUp folder/task-list workflows, use supported tools such as `CLICKUP_GET_FOLDERS` and `CLICKUP_GET_TASKS`. If a more specific ClickUp endpoint is missing, request that tool through the standard tool-request flow. --- # Confluence (/kb/guide/toolkits-confluence) Use this guide to configure Confluence OAuth, execute tools with the correct account, and read, update, or download Confluence content. ## Configure Confluence OAuth [#configure-confluence-oauth] **Align custom OAuth scopes with the endpoint type.** For Confluence custom OAuth, keep Atlassian scopes aligned with the scopes Composio expects. Classic and granular scopes differ depending on whether the underlying Confluence endpoint is v1 or v2. Incorrect substitutions such as using an irrelevant space scope can cause tool execution errors even if OAuth completes. **Add `offline_access` when refresh tokens are needed.** For Confluence OAuth, include the `offline_access` scope in the auth config and then create a new connected account. `offline_access` enables token refresh, and adding it to an existing auth config only affects new connections after users reconnect. **Use the same redirect URI in Composio and Atlassian.** The redirect URI in the Composio auth config and the Atlassian OAuth app must match. Copy the callback shown by the current auth-config flow or documentation; do not reuse legacy v1 or v3 callback paths from older examples. ## Execute Confluence tools with the correct account [#execute-confluence-tools-with-the-correct-account] **Pass the connected account ID, not the auth config ID.** For Confluence tool execution, pass the connected account ID. Do not pass the auth config ID/integration ID in the connected account field. Older SDK versions may also require the UUID form rather than the nano ID, so verify the SDK version and expected ID format. **Read supported scopes from MCP tool annotations.** For supported MCP deployments, Confluence scopes can be retrieved from the `annotations` field in the `listTools` API response. ## Read and update Confluence pages [#read-and-update-confluence-pages] **Retrieve page content by page ID.** Use `CONFLUENCE_GET_PAGE_BY_ID` to retrieve Confluence page content by page ID. This is the tool support shared for page body retrieval. **Fetch the latest page version before an update.** Confluence page updates require the correct page version. Pair `CONFLUENCE_UPDATE_PAGE` with `CONFLUENCE_GET_PAGE_VERSIONS` so the agent can fetch the latest required version and then update the page. By default, the agent should update over the latest version unless a specific version is requested. ## Download Confluence attachments [#download-confluence-attachments] Use `CONFLUENCE_GET_ATTACHMENTS` to list attachments and get the attachment ID, then pass that ID to `CONFLUENCE_DOWNLOAD_ATTACHMENT` to download the file. --- # Databricks (/kb/guide/toolkits-databricks) ## Databricks OAuth client and secret setup reference [#databricks-oauth-client-and-secret-setup-reference] For Databricks OAuth client and secret setup, follow the [official Databricks OAuth application guide](https://docs.databricks.com/aws/en/agents/mcp/connect-clients). An account administrator creates the OAuth application, configures its redirect URL and scopes, and securely records the generated client ID and client secret. ## Enter Databricks API key credentials during connected account linking [#enter-databricks-api-key-credentials-during-connected-account-linking] The Databricks API key credentials are entered during the connection flow. In code, point customers to `composio.connected_accounts.link()` for creating the connected account and entering the API key details. --- # DigitalOcean authentication (/kb/guide/toolkits-digital-ocean) ## DigitalOcean supports managed OAuth2, custom OAuth2, or a personal access token [#digitalocean-supports-managed-oauth2-custom-oauth2-or-a-personal-access-token] The current `digital_ocean` toolkit supports OAuth2 and API-key authentication. Use Composio-managed OAuth for the standard connection flow. Use a custom DigitalOcean OAuth app when you need control over provider settings; register the exact callback URI shown by the current Composio flow. For API-key authentication, provide a DigitalOcean Personal Access Token in the `bearer_token` connection field. If OAuth fails before consent, compare the authorization request with your custom app registration and use the API-key path only when it matches your security requirements. --- # Discord (/kb/guide/toolkits-discord) ## Discord OAuth credentials do not have a fixed expiration period [#discord-oauth-credentials-do-not-have-a-fixed-expiration-period] Discord OAuth2 client credentials do not have a fixed expiration period. If a customer-owned credential suddenly fails, it may have been manually revoked, reset, or regenerated in Discord. Verify the current Discord developer-app credentials and create a fresh connection before treating the failure as a broader provider or Composio issue. --- # Discord Bot (/kb/guide/toolkits-discordbot) ## Discord and DiscordBot use different token types [#discord-and-discordbot-use-different-token-types] Discord has two different authorization models: a user token represents an individual Discord user, while a bot token represents a bot account inside Discord. Composio separates these into different toolkits because the credentials and API behavior are different. Use the Discord toolkit for user-authorized actions and DiscordBot when the workflow needs to act as a Discord bot. ## Verify Discord auth config scopes when bot actions do not respond [#verify-discord-auth-config-scopes-when-bot-actions-do-not-respond] For DiscordBot behavior that does not respond as expected, verify that the Discord auth config includes the necessary scopes and permissions for the action being tested. Discord's OAuth2 documentation should be used as the source for the required scopes. If scopes look correct and the issue persists, collect the connected account ID or log ID for debugging. --- # DocuSign (/kb/guide/toolkits-docusign) ## DocuSign auth guide URL [#docusign-auth-guide-url] Use the DocuSign authentication guide at [https://composio.dev/auth/docusign](https://composio.dev/auth/docusign) for the current Composio DocuSign setup instructions. --- # Dropbox (/kb/guide/toolkits-dropbox) ## Allow the Composio auth-app redirect URL in the Dropbox app [#allow-the-composio-auth-app-redirect-url-in-the-dropbox-app] For Dropbox OAuth setup, configure the Dropbox app with the exact callback shown by the current Composio auth-config flow. Do not use the legacy v1 auth-app callback from older examples. ## Dropbox connections use Dropbox native OAuth, not Microsoft/Azure/Outlook login [#dropbox-connections-use-dropbox-native-oauth-not-microsoftazureoutlook-login] The Dropbox integration uses Dropbox's native OAuth2 flow, so users authenticate through Dropbox's login page. Composio cannot add Microsoft, Azure, or Outlook as alternative identity providers for Dropbox because the authentication method is controlled by Dropbox's API. If the customer's Dropbox Business tenant has SSO configured with Microsoft/Azure AD, that SSO behavior must be configured in Dropbox, not in Composio. ## For Dropbox upload, `path` is the Dropbox destination and `content` is the local file path [#for-dropbox-upload-path-is-the-dropbox-destination-and-content-is-the-local-file-path] For the Dropbox upload action, `path` is the destination path inside Dropbox, while `content` is the local file path that should be uploaded. Provide the local file path in `content`. ## Pass file paths to SDK attachment arguments rather than base64/file metadata objects [#pass-file-paths-to-sdk-attachment-arguments-rather-than-base64file-metadata-objects] When using the SDK attachment argument for supported email tools, pass a file path rather than an object containing filename, data, and content type. The SDK handles the file path. If the source file is available at a Dropbox-backed path, pass that Dropbox file path directly in the attachment argument. ## Use DROPBOX\_GET\_ABOUT\_ME to confirm which Dropbox account is connected [#use-dropbox_get_about_me-to-confirm-which-dropbox-account-is-connected] If Dropbox files or folders appear to be missing after a successful operation, confirm the connected Dropbox account before deeper debugging. Use `DROPBOX_GET_ABOUT_ME` to inspect the account tied to the active Composio connection and compare it with the Dropbox account the user is checking manually. --- # Excel (/kb/guide/toolkits-excel) Use this guide to pass valid Excel workbook inputs, operate on SharePoint-backed files, and keep Microsoft auth and tool schemas current. ## Pass valid inputs to Excel workbook actions [#pass-valid-inputs-to-excel-workbook-actions] **Send range values as a two-dimensional array.** Pass values as a two-dimensional array, where the outer list represents rows and each inner list contains the cell values for that row. Even a single cell must be wrapped twice, for example \{"values": \[\["92"]]}. **Use structured workbook data for uploads.** Use the revamped Excel tool shape that accepts structured data through worksheet\_names and worksheet\_data lists/dicts. The tool generates the .xlsx file before upload, instead of requiring the caller or LLM to provide binary workbook content directly. **Check the worksheet name and workbook item ID when `EXCEL_GET_RANGE` fails.** If get range appears to fail while the tool itself is working, verify that the workbook actually has the requested worksheet name, such as Sheet1, and that the item\_id being passed is the correct file ID for that workbook. **Use Excel actions for SharePoint-backed workbook operations.** For workbook operations, use the Excel toolkit actions because they are Excel APIs. Support identified EXCEL\_CLOSE\_SESSION, EXCEL\_DELETE\_WORKSHEET, EXCEL\_UPDATE\_WORKSHEET, and EXCEL\_UPDATE\_RANGE as already supported for the remaining Excel use cases. ## Configure Excel authentication and current tool schemas [#configure-excel-authentication-and-current-tool-schemas] **Use the shared Microsoft auth guide.** For Excel auth setup, use the Microsoft auth guide published at [https://composio.dev/auth/outlook](https://composio.dev/auth/outlook). The same guide applies to SharePoint, Microsoft Teams, Outlook, and Excel. **Upgrade schemas that expose dollar-sign parameters.** Upgrade to the latest available release when an older Excel schema exposes dollar-sign parameter names that the model provider rejects. The current schema no longer uses those invalid top-level parameter names. **Use current versions for column formatting and wrapping.** Generic column wrapping and related sheet operations are available in the current Excel toolkit. If they are missing from the action schema, switch to the latest toolkit version. --- # Facebook (/kb/guide/toolkits-facebook) ## FACEBOOK\_DELETE\_POST failure can be fixed by using the latest Facebook toolkit version [#facebook_delete_post-failure-can-be-fixed-by-using-the-latest-facebook-toolkit-version] If `FACEBOOK_DELETE_POST` fails on an older pinned toolkit version, try the latest Facebook toolkit version first. Remove the historical pin or pass `latest` according to the SDK/API path being used. ## Meta OAuth issues can require adding the Composio redirect URI in the Meta app settings [#meta-oauth-issues-can-require-adding-the-composio-redirect-uri-in-the-meta-app-settings] For Meta/Facebook OAuth failures, verify that the Composio redirect URI is added to the correct redirect URI field in the Meta developer app settings. If the app is using custom credentials, the redirect URI in Meta must match the Composio callback URI used by that auth config. After adding it, retry the connection flow. ## Facebook or Instagram connections authenticate the account selected in Meta's picker and cannot be repointed server-side [#facebook-or-instagram-connections-authenticate-the-account-selected-in-metas-picker-and-cannot-be-repointed-server-side] If the wrong Facebook/Page/Instagram account is connected, remove the existing Composio/Meta app authorization from Facebook's Business Integrations or business tools settings, sign out of other Facebook accounts or use a clean browser profile, then reconnect and choose the correct account/Page/Instagram asset in Meta's picker. Composio cannot manually switch the underlying account for an already-issued Meta token. ## WhatsApp connections require WABA ID as generic\_id, with bearer\_token for API-key auth [#whatsapp-connections-require-waba-id-as-generic_id-with-bearer_token-for-api-key-auth] For WhatsApp, OAuth2 connection initiation requires `generic_id`, which is the WABA ID. API-key auth requires both `bearer_token` (System User Token) and `generic_id` (WABA ID). Customers can find the WABA ID in the Facebook developer app's WhatsApp API setup, or through Meta APIs such as `/me/businesses` followed by `/{business_id}/owned_whatsapp_business_accounts`. To avoid hardcoding, use hosted auth links so the user can enter required fields during connection. --- # Fathom (/kb/guide/toolkits-fathom) ## Fathom and granola\_mcp are supported meeting transcription toolkits; request unsupported tools separately [#fathom-and-granola_mcp-are-supported-meeting-transcription-toolkits-request-unsupported-tools-separately] The `fathom` and `granola_mcp` toolkits are supported. If a requested meeting transcriber is not available, such as Otter, direct the customer to submit the request at `https://request.composio.dev/`. ## OAuth authorization URLs are provider-specific [#oauth-authorization-urls-are-provider-specific] Authorization URLs depend on the provider/toolkit involved in the connection flow. When troubleshooting OAuth redirects, check which provider the auth config and connection flow resolve to before treating a provider-specific authorization domain as inherently incorrect. --- # Figma (/kb/guide/toolkits-figma) Use this guide to configure Figma authentication, discover available tools, and work with design tokens and components. ## Configure Figma authentication for production [#configure-figma-authentication-for-production] **Let Composio handle Bearer authorization.** For Figma, customers can provide the supported credentials/token through the toolkit's auth mode, and Composio handles the Bearer authorization header internally. They should not need to manually create a separate Bearer-token auth scheme for normal Figma tool use. **Use customer-owned credentials for production rate limits.** If Figma returns 429, verify the response is coming from Figma and review Figma's rate-limit docs. Composio's default Figma app is fine for testing, but production use should use the customer's own Figma credentials to avoid shared-app pressure and to control scopes/rate limits. **Remove deprecated scopes before reconnecting.** If a Figma auth config contains the deprecated `file_read` scope, remove it and initiate a new connection. ## Discover and run Figma tools across auth modes [#discover-and-run-figma-tools-across-auth-modes] Figma tools should be usable regardless of whether the connection uses Composio-managed OAuth, a custom OAuth app, or token/API-key auth. If you cannot find a tool, fetch available tools dynamically and check the auth scopes required by that tool. ## Work with Figma design tokens and components [#work-with-figma-design-tokens-and-components] **Check plan access when extracting variables.** Some Figma API features are plan-limited. If `FIGMA_EXTRACT_DESIGN_TOKENS` fails when `include_variables` is enabled, verify your Figma plan/API access. As a workaround, set `include_variables` to false. **Use current design-token and component actions.** For Figma design-token and component workflows, use `FIGMA_EXTRACT_DESIGN_TOKENS`, `FIGMA_DESIGN_TOKENS_TO_TAILWIND`, and `FIGMA_GET_FILE_NODES`. The older `FIGMA_GET_COMPONENT` action is deprecated. If a needed Figma tool is missing, submit it through the Composio request portal. --- # Firecrawl (/kb/guide/toolkits-firecrawl) Use this guide to connect Firecrawl with an API key, discover and run its tools, configure scrape endpoints and timeouts, and use it through Connect MCP. ## Connect Firecrawl with an API key [#connect-firecrawl-with-an-api-key] **Use API-key auth instead of OAuth.** Firecrawl does not use a Composio-managed OAuth/test connector flow. It is an API-key toolkit, so you need a Firecrawl API key and, in many cases, your own Firecrawl subscription/account. If an MCP client does not prompt for the key, provide it through the connection flow or explicitly tell the agent/client to use the Firecrawl API key for authentication. **Create the connected account with `generic_api_key`.** For Firecrawl API-key auth, create the connected account with `authScheme: "API_KEY"` and a value object containing `status: "ACTIVE"` and `generic_api_key: "fc-..."`. The exact required key names can be checked from the toolkit metadata/connection initiation fields. ## Discover and run Firecrawl tools [#discover-and-run-firecrawl-tools] **Increase the tool-list limit when actions are missing.** If `FIRECRAWL_SEARCH` or other Firecrawl tools are missing from a tools list, increase the list limit or paginate. The default list can return only the first 20 tools, so request a higher limit such as `limit=1000` when fetching Firecrawl tools. **Choose the retrieval tool that matches the task.** For website content retrieval with Firecrawl, use `FIRECRAWL_SCRAPE` to scrape page content or `FIRECRAWL_EXTRACT` for extraction-style workflows. For broader web search, Composio Search may be a better fit depending on the use case. ## Configure endpoints and scrape timeouts [#configure-endpoints-and-scrape-timeouts] **Batch fewer URLs or raise the timeout for long scrape jobs.** For Firecrawl scrape timeouts, reduce the number of links per request, such as batching 1-2 links at a time for complex pages, or increase the scrape timeout if the tool call supports it. A useful starting value is `timeout: 120000` for roughly a 2-minute timeout. **Use the Firecrawl v1 API base URL.** The Firecrawl API base URL is `https://api.firecrawl.dev/v1`. If you must manually enter a base URL to unblock a connection or custom call, use that value. If the toolkit should have supplied it automatically, contact Composio support with the connection details. ## Use Firecrawl with Connect MCP [#use-firecrawl-with-connect-mcp] **Connect Firecrawl separately for each consumer account.** For Connect MCP on the For You side, each user's MCP session is tied to their own Composio consumer account, not the shared workspace context. A Firecrawl connection created under one user's account/workspace will not automatically appear for colleagues in Claude. Each colleague should create/connect their own Firecrawl account connection for their individual Connect MCP session. --- # Gemini (/kb/guide/toolkits-gemini) Use this guide to choose supported Gemini models, handle generated media, connect through MCP, and troubleshoot provider compatibility. ## Choose supported models and handle generated media [#choose-supported-models-and-handle-generated-media] **Use current Gemini model names.** If Gemini tool calls fail with older model names, switch to a currently supported Gemini model. For example, use `gemini-2.5-flash` instead of the older `gemini-1.5-flash`; model availability changes over time. **Choose a supported Veo model for video generation.** For Gemini video generation, use supported Veo models such as `veo-3.1-generate-preview`, `veo-3.1-fast-generate-preview`, `veo-3.0-generate-001`, or `veo-3.0-fast-generate-001`. If the default model fails, explicitly pass a current supported Veo model. **Wait for asynchronous video generation to complete.** Gemini video generation is asynchronous. Pass the `operation_name` returned by `GEMINI_GENERATE_VIDEOS` to `GEMINI_WAIT_FOR_VIDEO`, which polls for completion and returns the generated video file. The older `GEMINI_GET_VIDEOS_OPERATION` action is deprecated. **Disable automatic file handling when outputs should remain as URLs or content.** Composio SDKs automatically handle file upload/download by default. For Gemini generated images or similar file outputs, disable automatic file handling with `autoUploadDownloadFiles: false` / `auto_upload_download_files=False` where supported, or update to a version that supports that option. ## Connect Gemini through MCP and frameworks [#connect-gemini-through-mcp-and-frameworks] **Use Tool Router with any compatible MCP client.** Tool Router can be used with any MCP client or framework/LLM that supports tool calling or MCP. For Gemini, initialize Composio with `GeminiProvider`, create a session, then connect to the session MCP URL and headers using a streamable HTTP MCP client. **Isolate Gemini CLI-specific MCP failures.** If a Composio MCP server URL returns tools but Gemini CLI still fails, the issue may be in the Gemini client. Try the latest Gemini CLI version and, if needed, compare with another MCP client to isolate whether the failure is client-specific. **Use LangChain MCP tools with any capable model.** Composio MCP tools with LangChain are not limited to OpenAI. They can work with any LLM/framework path that supports LangChain function calling capabilities, including Gemini and Claude. ## Check provider compatibility and tool-call accounting [#check-provider-compatibility-and-tool-call-accounting] **Account for no-auth toolkit calls like regular tool calls.** Gemini no-auth toolkit calls are logged like other toolkit calls and can be tracked in Composio tool logs. Confirm the current plan's tool-call accounting when answering billing questions because pricing can change. **Check Google's schema limitations when otherwise-valid tools fail.** Gemini models/providers can have schema compatibility issues because Gemini uses OpenAPI-style schema handling rather than full JSON Schema support in some paths. If a schema works in OpenAI/Claude but fails in Gemini, check provider schema limitations and upgrade Composio/provider SDKs where fixes exist. **Verify current language-specific provider support.** Composio supports Google Gemini and Vertex AI providers. Verify the current SDK version and provider documentation when answering implementation-specific questions because language-specific support changes over time. --- # GitHub (/kb/guide/toolkits-github) ## GitHub V2 triggers do not require creating a webhook endpoint first [#github-v2-triggers-do-not-require-creating-a-webhook-endpoint-first] GitHub V2 trigger setup does not require a separate webhook endpoint creation step. The webhook URL is automatically provisioned when the trigger instance is created. Skip the `/webhook_endpoints` call and create or update the trigger directly through `/trigger_instances/{slug}/upsert`. ## List GitHub organizations and repositories for the authenticated user [#list-github-organizations-and-repositories-for-the-authenticated-user] Use `GITHUB_LIST_ORGANIZATIONS_FOR_THE_AUTHENTICATED_USER` to list organizations available to the authenticated GitHub user. Then use `GITHUB_LIST_ORGANIZATION_REPOSITORIES` to list repositories for a selected organization. During connection, the user should be able to choose the organization they want to grant access to. ## GitHub connected-account tokens are redacted from API responses [#github-connected-account-tokens-are-redacted-from-api-responses] Provider tokens are redacted from connected-account API responses for both Composio-managed and customer-owned auth configs. Use Composio tool execution or Proxy Execute when a workflow needs to call GitHub; do not build a flow that reads the OAuth token from connected-account data. ## GitHub organization access can require approval from an organization owner [#github-organization-access-can-require-approval-from-an-organization-owner] If a GitHub connection works for personal repositories but cannot access an organization, check whether that organization restricts OAuth app access. The user can open GitHub **Settings → Applications → Authorized OAuth Apps**, select the OAuth app, and request access for the organization. An organization owner must approve the request in GitHub; reconnecting in Composio does not bypass the organization's policy. GitHub documents the member request flow at [https://docs.github.com/en/account-and-profile/how-tos/organization-membership/requesting-organization-approval-for-oauth-apps](https://docs.github.com/en/account-and-profile/how-tos/organization-membership/requesting-organization-approval-for-oauth-apps) and the owner approval flow at [https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data/approving-oauth-apps-for-your-organization](https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data/approving-oauth-apps-for-your-organization). ## Session-level tool allowlists are enforced server-side at execution time [#session-level-tool-allowlists-are-enforced-server-side-at-execution-time] Session-level restrictions are enforced server-side at execution time. When a session is configured with `toolkits`, `tools`, or `tags`, every execution request is validated against the enabled or disabled toolkit list, per-toolkit tool list, and tag filters. Disabled tools are filtered from search results, and execution is blocked before the provider API call if the tool fails validation. ## Use custom OAuth credentials for branded GitHub consent and redirect flows [#use-custom-oauth-credentials-for-branded-github-consent-and-redirect-flows] Composio supports white-labeling the hosted auth page by customizing the logo and app name in Project Settings > Auth Screen. For provider OAuth consent screens such as GitHub, use your own OAuth app credentials so the provider consent screen shows your brand instead of Composio's shared OAuth app. Redirect URLs can also be routed through your own domain so users do not see a Composio domain during the redirect path. --- # Gmail (/kb/guide/toolkits-gmail) Use this guide to configure Gmail authentication, send and fetch messages, work with attachments and labels, and set up new-message triggers. ## Configure Gmail OAuth, scopes, and toolkit versions [#configure-gmail-oauth-scopes-and-toolkit-versions] **Use `latest` or v3.1 for newer Gmail settings tools.** The v3 execute endpoint can default to base toolkit version `00000000_00` when no version is specified. For newer Gmail tools like `GMAIL_PATCH_SEND_AS`, `GMAIL_LIST_SEND_AS`, and `GMAIL_GET_VACATION_SETTINGS`, pass `version: "latest"` in the execute body or use the v3.1 endpoint, which defaults to latest. **Create the auth config before initiating a connection.** Create the Gmail auth config first with the custom OAuth credentials, then initiate a connected account using that auth config. The callback URL is supplied during connection initiation, while the OAuth client ID/secret and redirect URI live on the auth config. **Choose scopes based on the actions and data required.** When creating the Gmail auth config, pass the desired Gmail scopes in `credentials.scopes`, typically as a comma-joined string. Example scopes include `gmail.send`, `gmail.readonly`, `gmail.compose`, `gmail.modify`, and `gmail.labels`. Gmail filter creation maps to the Gmail API `users.settings.filters.create` endpoint: `POST /gmail/v1/users/{userId}/settings/filters`. Google lists `https://www.googleapis.com/auth/gmail.settings.basic` as the required OAuth scope for this endpoint, and the current Composio `GMAIL_CREATE_FILTER` action declares the same single required scope. Google must approve this scope for the OAuth app used by the connection. If the consent screen blocks an unverified scope, use an OAuth app that is verified for `gmail.settings.basic` and reconnect. `https://www.googleapis.com/auth/gmail.send` can send messages, but it is a granular sensitive scope and requires Google verification. The broader `https://mail.google.com/` scope gives full mailbox access and can cover send use cases, but it is broader than many customers want. The Gmail metadata scope cannot be used when requesting full email content. Remove `https://www.googleapis.com/auth/gmail.metadata` and use a scope that allows message content access, such as `https://mail.google.com/`, when full payload/body data is needed. **Use Google Super for one Google connection across services.** Google Super owns the canonical multi-service authentication guidance. See [Google Super is a unified Google Workspace toolkit](/kb/guide/toolkits-googlesuper). ## Address and send Gmail messages [#address-and-send-gmail-messages] **Use `me` for the authenticated user.** For Gmail tool calls, `me` can be used as the `user_id` to refer to the authenticated connected account. **Provide at least one recipient channel.** `GMAIL_SEND_EMAIL` no longer needs a single required recipient field. At least one recipient channel such as `to` / `recipient_email`, `cc`, or `bcc` can be supplied, which keeps the tool flexible for different email composition flows. For hosted MCP / Tool Router calls through `COMPOSIO_MULTI_EXECUTE_TOOL`, put recipient fields inside the nested tool `arguments` object. Prefer `recipient_email` for the first To recipient and `extra_recipients` for additional To recipients unless the current schema explicitly exposes another shape. If the connection is active but the action returns `At least one of 'to' (or 'recipient_email'), 'cc', or 'bcc' must be provided`, the tool did not receive a recipient channel and failed before Gmail API execution. Retry with the exact nested `recipient_email` shape; if it still fails, provide a fresh request ID for investigation. **Select a send-as alias with `from_email`.** Use the `from_email` parameter on `GMAIL_SEND_EMAIL` to choose the Gmail send-as alias. ## Send attachments safely [#send-attachments-safely] **Upload files before tool execution.** Temporary S3/file instances are short-lived. Use `files.upload` before tool execution via the SDK or MCP flow, then pass the resulting `FileUploadable`/uploaded file object to the agent/tool call. **Verify a timed-out send before retrying.** `GMAIL_SEND_EMAIL` accepts attachments as uploaded Composio file references, not signed URLs or JSON strings. The action downloads the uploaded file, builds the MIME message, base64-url encodes it, and posts it to Gmail. Attachment sends can therefore take materially longer than small text-only sends. Current Python and TypeScript SDKs do not automatically retry non-idempotent tool executions. However, a client timeout can still occur after Gmail accepted the message. If `GMAIL_SEND_EMAIL` hangs or creates duplicate sends with attachments: * If the log is a fast 400 validation error, verify the `attachment` argument is an object/list with `name`, `mimetype`, and `s3key`. * If the client timed out, inspect the Composio execution log or Gmail Sent folder before retrying manually. * If the client is older than Python SDK 0.16.0 or TypeScript SDK 0.14.0, upgrade before investigating SDK-level automatic retries. ## Fetch messages and manage labels [#fetch-messages-and-manage-labels] **Reduce fetch payload size.** For Gmail fetch/list flows, set `include_payload=false` and `verbose=false` where supported. For very lightweight flows, use `only_ids=true` and then fetch selected messages separately. Also use `max_results` and Gmail `query` filters to keep result sets small. **Use label IDs for label operations.** For Gmail label operations and trigger label filters that require IDs, pass the label ID rather than the display name. Use `GMAIL_LIST_LABELS` to retrieve IDs. **Use accepted Gmail color values when patching labels.** To patch a label color, use the label ID and pass background color as an object field such as `{ "background_color": "#FFFF0000" }`. Gmail only accepts specific label color values from the Gmail API reference. ## Configure Gmail new-message trigger filters [#configure-gmail-new-message-trigger-filters] Use a Gmail query such as `label:sent OR label:category_personal` to filter matching messages. This avoids depending on label IDs for that trigger path. --- # Gong (/kb/guide/toolkits-gong) ## Gong base URL differs by customer and should be provided at connection time [#gong-base-url-differs-by-customer-and-should-be-provided-at-connection-time] Gong's base URL can differ per user/customer. Avoid hardcoding a single Gong base URL in a shared auth config for all users; collect and pass the user's `gong_url`/base URL when initiating the connected account. ## Gong connection initiation can use Basic auth fields: access key, access key secret, and Gong URL [#gong-connection-initiation-can-use-basic-auth-fields-access-key-access-key-secret-and-gong-url] For Gong Basic auth, collect the access key as username, access key secret as password, and the customer's Gong URL/base URL. Pass those fields when initiating the connected account; hosted auth can also collect required fields for the customer instead of manually building the frontend form. ## Gong MCP tool scopes can be read from tool annotations in the tools API [#gong-mcp-tool-scopes-can-be-read-from-tool-annotations-in-the-tools-api] For Gong MCP tools, scopes are exposed through the `annotations` field from the `listTools` API per the newer MCP spec. To determine Gong scopes, inspect tool annotations from the tools API instead of relying only on static docs. --- # Google Analytics (/kb/guide/toolkits-google-analytics) ## Use latest toolkit version when Google Analytics tools return ToolNotFound or only a few tools [#use-latest-toolkit-version-when-google-analytics-tools-return-toolnotfound-or-only-a-few-tools] If Google Analytics tools return `ToolNotFound` or the tools API only returns a small subset of Google Analytics tools, pass the latest toolkit version. For tools listing, use query params like `toolkit_versions=latest&toolkit_slug=google_analytics&limit=1000`. Older pinned/default versions can expose far fewer tools than the latest version. ## Add Google Analytics to an MCP config as a selected tool/toolkit [#add-google-analytics-to-an-mcp-config-as-a-selected-tooltoolkit] To use Google Analytics through MCP, create an MCP config with Google Analytics selected, or edit an existing MCP config and add Google Analytics as a tool/toolkit. Then follow the MCP quickstart to connect and use the generated MCP configuration. ## Empty Google Analytics reports may be provider data availability rather than Composio failure [#empty-google-analytics-reports-may-be-provider-data-availability-rather-than-composio-failure] If Google Analytics report tools return no data or unexpected data, compare the same property, date range, dimensions, and metrics through Google Analytics itself or a Proxy Execute request. If the provider returns the same empty result, it is likely a data-availability or query issue. If the equivalent provider request works but the Composio tool does not, contact Composio support with the log ID and a redacted comparison. Never extract or share a token from connected-account data. --- # Google Calendar (/kb/guide/toolkits-google-calendar) Use this guide to connect Google Calendar, work with event data and availability, configure triggers, and troubleshoot version-specific behavior. ## Connect a Google Calendar account [#connect-a-google-calendar-account] Create a Google Calendar integration/auth config, connect the account, and then use the Google Calendar toolkit's tools and triggers through that connected account. Ensure the connected account has `https://www.googleapis.com/auth/calendar.events` when calling event-list or event-fetch tools that require event access. ## Work with events and availability [#work-with-events-and-availability] **Update RSVP status with the attendee-list limitation in mind.** Google Calendar can limit RSVP/status updates when an event has multiple attendees. Update the authenticated user's RSVP and then re-add the attendees, or resend the attendee list with the updated status. **Use Find Free Slots for processed availability.** Query-free/busy returns provider data without extra processing such as timezone handling, so callers using free/busy may need to process it themselves. **Use `primary` as the calendar ID.** For Google Calendar tools, use a calendar ID such as `primary`; `me` is not a valid Google Calendar ID. **Read generated meeting links from `hangout_link`.** After creating or updating a calendar event with conferencing, read the generated meeting URL from the response's `hangout_link` field. ## Configure Google Calendar triggers [#configure-google-calendar-triggers] **Handle canceled or deleted events.** `GOOGLECALENDAR_EVENT_CANCELED_DELETED_TRIGGER` sends a payload when an event is canceled or deleted. **Create separate trigger instances for separate calendars.** Multiple triggers for the same trigger slug and user are supported when each trigger is configured for a different `calendarId`. **Expect full event data from newer triggers.** The newer Google Calendar new-event trigger payload includes complete event data rather than only the event ID. Google Calendar trigger behavior moved from webhook-style delivery toward polling so payloads can include more detail and require less follow-up processing. Existing trigger flows were preserved while polling could be introduced separately where needed. **Retrieve trigger metadata programmatically.** Use the trigger-types endpoint to retrieve Google Calendar trigger metadata programmatically, and use the triggers documentation for setup guidance. ## Troubleshoot ignored event filters [#troubleshoot-ignored-event-filters] Older pinned Google Calendar toolkit versions can drop or remap filters such as `timeMin` and `timeMax` before the request reaches Google. Use the latest toolkit version or v3.1/latest behavior when filter changes produce identical results. --- # Google Classroom (/kb/guide/toolkits-google-classroom) Use this guide to choose managed or customer-owned Google OAuth for Google Classroom and troubleshoot consent, scope, or token failures. ## Configure Google OAuth for Google Classroom [#configure-google-oauth-for-google-classroom] **Follow the Google Apps credential setup guide for custom OAuth.** For a step-by-step guide to creating and configuring Google OAuth credentials with Composio, see [How to create OAuth2 credentials for Google Apps](https://composio.dev/auth/googleapps). **Enable the Google Classroom API for custom OAuth.** Enable the Google Classroom API in the Google Cloud project that owns the credentials. After enabling it under **APIs & Services**, wait a few minutes and retry. **Choose managed or customer-owned OAuth.** Use Composio-managed OAuth for the standard connection flow. Use a custom Google OAuth app when you need control over scopes, consent-screen branding, or Google Cloud project policy. For custom OAuth, configure the app name and branding in that project and use the redirect URL shown by Composio's current auth-config flow. ## Troubleshoot Google Classroom OAuth and tool calls [#troubleshoot-google-classroom-oauth-and-tool-calls] **Remove unverified scopes when Google reports “App is blocked.”** This error usually means the OAuth client is requesting scopes that Google has not verified for that client. Remove additional scopes beyond the defaults, or use a custom OAuth app and submit the scopes for verification. **Validate scopes when OAuth returns `Error 400: invalid_scope`.** Verify the requested scopes and their formatting against the [Google OAuth scopes documentation](https://developers.google.com/identity/protocols/oauth2). **Reconnect when tool calls return 401.** A 401 usually means the access token is no longer valid. The user may have revoked access, changed password or two-factor settings, been affected by an administrator policy, or exceeded Google's refresh-token limit. Re-authenticate the connected account and retry. --- # Google Maps (/kb/guide/toolkits-google-maps) ## Maps Embed API requires API-key authentication [#maps-embed-api-requires-api-key-authentication] `GOOGLE_MAPS_MAPS_EMBED_API` requires API-key authentication. Use an auth config whose auth mode is `api-key`, or pass the `api_key` parameter directly when making the tool call. ## Google Maps OAuth can be blocked by sensitive cloud-platform scope [#google-maps-oauth-can-be-blocked-by-sensitive-cloud-platform-scope] Check whether the OAuth app requests the sensitive `https://www.googleapis.com/auth/cloud-platform` scope. If the Google OAuth app has not been verified, users who are not listed as test users and are outside the registering organization can be blocked by Google. Either complete Google verification or ensure the affected users are allowed test/org users for that OAuth app. ## Validate Places `includedTypes` against Google's supported place types [#validate-places-includedtypes-against-googles-supported-place-types] For Google Maps Places requests, `includedTypes` must use values supported by Google's Places API. If a request fails with an invalid argument around `includedTypes`, compare the value against Google's supported place type lists and replace unsupported values before retrying. ## Deprecated `GEOCODING_API` is not the Google Maps toolkit tool to use [#deprecated-geocoding_api-is-not-the-google-maps-toolkit-tool-to-use] `GEOCODING_API` belongs to a different toolkit and has been deprecated. Do not require it as part of normal `google_maps` toolkit usage; use the current Google Maps toolkit tool slugs instead. ## Google Maps APIs may require billing and quota management in GCP [#google-maps-apis-may-require-billing-and-quota-management-in-gcp] Most Google APIs used through Composio are generally free to access, but Google Maps is an exception: Maps APIs can require billing on the Google Cloud project. If usage exceeds limits, customers may need to request higher limits in their own Google project. --- # Google Ads (/kb/guide/toolkits-googleads) ## Google Ads developer token now belongs on the auth config, not connection initiation [#google-ads-developer-token-now-belongs-on-the-auth-config-not-connection-initiation] Google Ads was changed so the developer token lives on the auth config itself, not on each connection initiation request. Older auth configs created before this change do not have the developer token field, and new connections through those auth configs can fail because the token is no longer accepted at the connection level. Create a new Google Ads authConfig with the developer token included, then create a fresh connection through that authConfig. ## Google Ads API requires both OAuth access token and developer token [#google-ads-api-requires-both-oauth-access-token-and-developer-token] Google Ads API requests require both an OAuth access token and a Google Ads developer token. For production reliability and isolated provider quota, customers should use their own Google Ads developer token where possible. ## Google Ads toolkit versions should be passed without the dashboard `v` prefix [#google-ads-toolkit-versions-should-be-passed-without-the-dashboard-v-prefix] The SDK expects toolkit version strings without the dashboard's leading `v`. If the dashboard shows `v`, pass `` in `toolkitVersions` or per-execution `version`. `dangerouslySkipVersionCheck` is a per-execution option inside the `tools.execute()` payload, not a constructor option. Sessions can manage toolkit versions automatically if the customer migrates to session-based execution. ## Google Ads MCC/sub-account customer ID targeting is supported [#google-ads-mccsub-account-customer-id-targeting-is-supported] The Google Ads toolkit now correctly supports an optional per-call `customer_id` for customer-scoped tools. * Pass the child/subaccount customer ID as `customer_id`; it becomes the target account in the Google Ads request path. * If `customer_id` is omitted, the tool falls back to the Customer ID stored on the connection. * When the requested customer differs from the connection Customer ID, the connection Customer ID can supply the MCC/manager context for Google's `login-customer-id` header unless that header is already present. * `GOOGLEADS_LIST_ACCESSIBLE_CUSTOMERS` is for account discovery. It can return accessible IDs, but later customer-scoped tools still need a selected target customer ID. If the request still fails, contact Composio support with the exact tool, request/log ID, manager/MCC customer ID, child customer ID, and Google error. The customer-ID override is already supported, so do not troubleshoot this as a pending feature. ## Campaign mutate 400s can be caused by unsupported inline Campaign fields [#campaign-mutate-400s-can-be-caused-by-unsupported-inline-campaign-fields] `GOOGLEADS_MUTATE_CAMPAIGNS` may fail with Google Ads 400 `INVALID_ARGUMENT` errors such as `Unknown name "dailyBudget" at operations[0].update` or `Unknown name "targetedLocations" ... Cannot find field`. These failures happen when the request includes fields that are not valid inline Campaign resource fields. Do not treat these as OAuth failures. Check the tool execution log for rejected payload fields. Google Ads does not accept `daily_budget`, `targeted_locations`, `exclusion_locations`, and related date/budget/location fields directly on the Campaign mutate body. Remove those inline fields and treat the error as a request-shape issue rather than an OAuth failure. A real daily budget requires a CampaignBudget resource (`campaignBudgets:mutate`) and then passing the CampaignBudget resource name through `campaign_budget`. Location targeting belongs in CampaignCriterion mutations, not inline Campaign fields. Example response: "The failure is in the Google Ads campaign-mutate payload shape, not your connection. Some inline campaign fields are being sent in a form that Google Ads rejects. Use CampaignBudget and CampaignCriterion mutations for budget and location targeting instead." ## Google Ads OAuth callback token-exchange failures usually point to incorrect credentials [#google-ads-oauth-callback-token-exchange-failures-usually-point-to-incorrect-credentials] The `OAuth callback failed during token exchange` error usually means the credentials used to complete the auth flow are incorrect, most often the client secret. Re-enter or update the client secret in the Google Ads auth config, make sure there are no leading/trailing spaces, and initiate a new connection. ## Custom Google OAuth apps need callback routing through the customer's domain for branded consent [#custom-google-oauth-apps-need-callback-routing-through-the-customers-domain-for-branded-consent] For Google toolkits, creating a new authConfig with the customer's OAuth app credentials is not enough for full white-label consent. They also need to route the callback through their own domain using their own redirect URI so Google displays the configured consent screen for that OAuth app. --- # Google BigQuery (/kb/guide/toolkits-googlebigquery) ## BigQuery supports managed OAuth2, custom OAuth2, and service-account auth [#bigquery-supports-managed-oauth2-custom-oauth2-and-service-account-auth] Use Composio-managed OAuth for the standard connection flow. Use a custom Google OAuth app when you need control over scopes, consent-screen branding, or Google Cloud project policy. Service-account authentication is also available; grant the service account only the BigQuery permissions required by the intended tools. If Google blocks an OAuth consent flow, check the OAuth app's verification, test-user, organizational-policy, and requested-scope settings before treating the failure as a Composio problem. Generate a fresh auth link after correcting the Google Cloud configuration. --- # Google Docs (/kb/guide/toolkits-googledocs) Use this guide to create and edit Google Docs content, configure Google OAuth, manage accounts and sessions, and connect through the correct Composio surface. ## Create and edit Google Docs content [#create-and-edit-google-docs-content] **Create documents from Markdown or HTML tables.** `GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN` accepts GitHub-Flavored Markdown. Markdown tables should work, and HTML tables can also be passed in the markdown payload when a table shape is needed. **Use the tab-aware tools for reading and editing.** Google Docs tab-level access is supported. For reading tabs, use `GOOGLEDOCS_GET_DOCUMENT_BY_ID` or `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT`. For editing specific tabs, use `GOOGLEDOCS_REPLACE_ALL_TEXT`, `GOOGLEDOCS_REPLACE_IMAGE`, or `GOOGLEDOCS_UPDATE_EXISTING_DOCUMENT`. ## Configure Google OAuth [#configure-google-oauth] **Choose managed or customer-owned OAuth2.** Use Composio-managed OAuth for the standard connection flow. Create a custom auth config with your Google OAuth app when you need control over scopes, consent-screen branding, or Google Cloud project policy. A Composio Project API key authenticates SDK/API calls to Composio; it is not a replacement for the user's Google OAuth grant. **Verify sensitive scopes for production use.** Google may block OAuth consent when an app requests unverified sensitive scopes. For production Google Docs or Workspace usage with sensitive scopes, use a verified OAuth app and complete the required Google verification or CASA process where applicable. Without verification, users may see warnings or app-blocked errors. **Execute through Composio instead of reading provider tokens.** Provider tokens are redacted from connected-account API responses. Use Composio tool execution or Proxy Execute instead of reading access or refresh tokens from connected-account data. ## Manage accounts, sessions, and auth configs [#manage-accounts-sessions-and-auth-configs] **Select explicitly when a user has multiple Google accounts.** Composio can keep multiple connected accounts for the same toolkit and user. Enable multi-account behavior for the session when needed, give each account a clear alias, and select the intended alias or connected-account ID during execution rather than relying on an implicit default. **Keep Tool Router v2 accounts under the same user or entity.** Tool Router v2 sessions are scoped to a single `user_id`. Every connected account passed into that session must belong to the same entity, otherwise validation fails with `ToolRouterV2_InvalidConnectedAccountIds`. Reconnect the outlier Google account under the same `user_id` or create a separate session. **Specify auth config IDs when creating the session.** When creating a Composio session, pass `auth_configs` keyed by toolkit slug, such as `gmail`, `googledrive`, or `googlecalendar`. If specified, Manage Connection uses those auth configs directly instead of picking a default config. ## Connect through Platform or Connect MCP [#connect-through-platform-or-connect-mcp] **Connect the app separately on each surface.** Connections created on Platform (`dashboard.composio.dev`) are isolated from For You / Connect MCP and do not carry over. To use Google Docs, Sheets, or Workspace through Connect MCP, ask the MCP server to connect the app from the client flow and complete that OAuth flow. --- # Google Drive (/kb/guide/toolkits-googledrive) Use this guide to upload and download Google Drive files, choose an execution path, configure OAuth and webhooks, and troubleshoot account or session issues. ## Upload and download Google Drive files [#upload-and-download-google-drive-files] **Pass local paths or URLs through SDK auto file handling.** For tools that support file-upload parameters such as `s3key`, `mimetype`, and `name`, the SDK can rewrite those parameters automatically. The caller can pass a local file path or URL string, and the SDK reads the file, uploads it to Composio-managed storage, and constructs the provider payload before executing the tool. For `GOOGLEDRIVE_UPLOAD_FILE`, passing `file_to_upload: "/path/to/file.pdf"` is the intended SDK pattern when auto file handling is enabled. **Plan for temporary download URLs and storage.** Downloaded files are staged in temporary S3-backed storage and exposed through presigned URLs. The default presigned URL TTL is 1 hour, and that URL expiration can be customized in Project Settings -> File TTL. The staged files themselves are short lived and are deleted from Composio storage after about 24 hours / one day. **Disable auto file handling when raw output is required.** If the SDK is converting downloaded file output into a local path and the application needs the raw URL or file payload, disable automatic file handling for the execution path. Use the documented `auto_upload_download_files=False` / disabling-auto-file-handling option, and make sure the relevant Composio SDK packages are upgraded to a version that supports that behavior. ## Choose MCP or direct execution [#choose-mcp-or-direct-execution] **Discover less common Connect MCP tools with meta-tools.** Connect MCP exposes a curated direct tool set so the assistant does not load hundreds or thousands of tools into context. Less common or higher-risk Google Drive actions, including `GOOGLEDRIVE_GOOGLE_DRIVE_DELETE_FOLDER_OR_FILE_ACTION`, should be discovered at runtime with `COMPOSIO_SEARCH_TOOLS` and executed with `COMPOSIO_MULTI_EXECUTE_TOOL`. **Prefer direct execution for a deterministic file-browser UI.** Using Composio MCP for a Google Drive file browser is feasible, but MCP servers are designed primarily for AI assistant integrations. For a product UI or deterministic file browser, prefer Direct Tool Execution through the Composio SDK or APIs so the application controls the tool calls, arguments, and rendering flow directly. ## Configure Google OAuth, scopes, and webhooks [#configure-google-oauth-scopes-and-webhooks] **Use a public endpoint for watch and change webhooks.** Google Drive webhook payloads need to be delivered to a public domain or publicly reachable endpoint. A private-domain listener is not sufficient for Composio's server to send the webhook payload. **Use customer-owned OAuth credentials with verified scopes.** Google can block the OAuth flow when the OAuth app is not verified for the requested sensitive or restricted scope. Configure and verify the required scope on the customer's Google Cloud OAuth app, then use those credentials in the Composio auth config. Also verify that the auth config requests only the intended scopes. **Choose the narrowest scope that supports the workflow.** The `drive.file` scope allows access to files the app creates or that the user explicitly grants to it. A workflow that needs broader full-drive access may require the `drive` scope on the customer's Google OAuth app. Configure and verify only the scopes the product actually needs. ## Troubleshoot account, toolkit, and session execution [#troubleshoot-account-toolkit-and-session-execution] **Check for an invalid toolkit version when a tool is missing.** If a Google Drive tool appears missing, check whether the request is pinned to a toolkit version that exists. Passing an invalid version such as a non-existent dated version can make tools unavailable. Retry with a valid Google Drive toolkit version, or use the latest version when a pinned version is not required. **Confirm the connected identity with `GOOGLEDRIVE_GET_ABOUT`.** Run `GOOGLEDRIVE_GET_ABOUT` for the connected account ID to confirm the email address and identity of the Google Drive account being used. This is the quickest check when actions appear to affect a different Drive account than expected. **Include an `arguments` object in execution requests.** When calling tool execution APIs such as `GOOGLEDRIVE_FIND_FILE`, include the `arguments` object in the request body. If the tool does not need arguments for that call, send an empty object such as `"arguments": {}` along with the connected account, user/entity ID, and version fields. **Keep every Tool Router v2 account under the same entity.** Tool Router v2 sessions are scoped to a single entity/user ID. Every connected account included in a session must belong to that same entity, otherwise validation can fail with `ToolRouterV2_InvalidConnectedAccountIds`. Reconnect Google Drive under the same user/entity as the Gmail and Calendar accounts before combining them in one session. If needed, specify auth config IDs while creating the session so Manage Connection uses the intended auth config for each toolkit. --- # Google Meet (/kb/guide/toolkits-googlemeet) ## Use Google Super tool slugs with a Google Super connected account [#use-google-super-tool-slugs-with-a-google-super-connected-account] Google Super is a separate toolkit with its own tool slugs. If the connected account was created for Google Super, run the corresponding GOOGLESUPER\_\* tool, such as GOOGLESUPER\_CREATE\_MEET, instead of the GOOGLEMEET\_\* slug. A separate Google Meet auth config or connected account is not required when the workflow is intentionally using Google Super. ## Configure Meet scopes and enable the Google Meet API before creating Meet spaces [#configure-meet-scopes-and-enable-the-google-meet-api-before-creating-meet-spaces] For Meet space creation/settings through Google Super, include the Meet scopes `https://www.googleapis.com/auth/meetings.space.created` and `https://www.googleapis.com/auth/meetings.space.settings` in the auth config, then initiate a new connection so the new scopes are granted. Also enable the Google Meet API in the Google Cloud Console project backing the OAuth app. ## Fetch transcript entries by first resolving the conference record [#fetch-transcript-entries-by-first-resolving-the-conference-record] Start with `GOOGLEMEET_LIST_CONFERENCE_RECORDS`. It can filter conference records by meeting code, space name, or time range. Use the resulting conference record ID with `GOOGLEMEET_GET_TRANSCRIPTS_BY_CONFERENCE_RECORD_ID`, then call `GOOGLEMEET_LIST_TRANSCRIPT_ENTRIES` with the transcript resource to retrieve the spoken segments. ## 403 permission errors usually mean the conference resource is inaccessible or missing [#403-permission-errors-usually-mean-the-conference-resource-is-inaccessible-or-missing] For a Google Meet API error like "Permission denied on resource Conference (or it might not exist)", verify that the signed-in connected account has access to the conference/artifact and that the conference record exists. Compare the provider response through a least-privileged Composio tool or Proxy Execute call; provider tokens are redacted from connected-account responses and should not be copied into a support workflow. ## Recordings and transcripts require an eligible Google Workspace edition and enabled feature [#recordings-and-transcripts-require-an-eligible-google-workspace-edition-and-enabled-feature] Google Meet recordings and transcripts are available on several eligible Google Workspace editions, not only Enterprise. The meeting host must have the feature, the organization's administrator must allow it, and recording or transcription must have been started for the meeting. Free personal accounts do not provide the same artifact availability. Check Google's current [Meet feature matrix](https://support.google.com/meet/answer/10459644) and [transcript requirements](https://support.google.com/meet/answer/12849897) when diagnosing a missing recording or transcript. --- # Google Sheets (/kb/guide/toolkits-googlesheets) Use this guide to connect Google Sheets, discover and run the current tools, and configure Google authentication and quotas. ## Connect Google Sheets and discover tools [#connect-google-sheets-and-discover-tools] **Connect separately through Platform and Connect MCP.** Connections made on the Platform side (`dashboard.composio.dev`) are isolated from the For You / `connect.composio.dev/mcp` flow. A Google Sheets connection created on Platform will not automatically appear in Connect MCP. To use Sheets through Connect MCP, ask the MCP server from the client to connect Google Sheets, complete the surfaced auth link, then retry discovery/execution. **Increase the tool-list limit when needed.** `get_raw_composio_tools` returns 20 tools by default. Pass a larger `limit` to fetch the full Google Sheets tool set, for example `.get_raw_composio_tools(toolkits=["GOOGLESHEETS"], limit=1000)`. **Use the spreadsheet ID for MCP operations.** The Google Sheets MCP flow does not search through spreadsheets by name. Provide the spreadsheet ID directly in the chat/tool call when asking for operations such as getting sheet names. ## Update and populate spreadsheets [#update-and-populate-spreadsheets] **Choose the current values tool for the operation.** Use `GOOGLESHEETS_VALUES_UPDATE` for one range, `GOOGLESHEETS_UPDATE_VALUES_BATCH` for multiple ranges, or `GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND` to append rows. To create and populate a new spreadsheet, call `GOOGLESHEETS_CREATE_GOOGLE_SHEET1` and then one of the current values-update actions. `GOOGLESHEETS_BATCH_UPDATE` and `GOOGLESHEETS_SHEET_FROM_JSON` are deprecated. **Execute tools with the exact current slug.** When executing Google Sheets tools, pass the exact current slug directly as the tool identifier, for example `composio.tools.execute("GOOGLESHEETS_GET_SHEET_NAMES", executePayload)`. If a wrapper parameter like `params.toolIdentifier` is used, verify it resolves to the exact tool slug. The older `GOOGLESHEETS_LIST_TABLES` action is deprecated. ## Configure Google authentication, versions, and quotas [#configure-google-authentication-versions-and-quotas] **Update old placeholder toolkit versions.** If Google Sheets actions fail with permission errors and logs show the base version `00000000_00`, switch to the latest Google Sheets toolkit version and check the toolkit versioning documentation. **Use Google Super for one shared Google connection.** For the canonical guidance on using one connection across Google Workspace services, see [Google Super is a unified Google Workspace toolkit](/kb/guide/toolkits-googlesuper). **Enter complete Google OAuth scope URLs.** When configuring Google scopes manually, use the full scope URL. For Drive access, use `https://www.googleapis.com/auth/drive` rather than shorthand values like `/drive`. **Treat Google provider quotas separately from Composio plan limits.** A Google Sheets 429 can come from Google's API quotas even when the Composio account has remaining tool calls. Google currently documents 300 read requests and 300 write requests per minute per project, plus 60 reads and 60 writes per minute per user per project. Apply exponential backoff and check [Google's current Sheets API limits](https://developers.google.com/workspace/sheets/api/limits) before relying on those numbers. --- # Google Slides (/kb/guide/toolkits-googleslides) Use this guide to discover, read, create, and connect Google Slides presentations in Composio. ## Discover and read Google Slides presentations [#discover-and-read-google-slides-presentations] **Discover presentations through Google Drive.** Google Slides does not offer a dedicated endpoint to list all presentations through the Slides toolkit. Use `GOOGLEDRIVE_FIND_FILE` and filter Drive files with `q`, for example `mimeType = 'application/vnd.google-apps.presentation'`, then pass the returned presentation ID into the Google Slides tool. **Pass the presentation ID to `GOOGLESLIDES_PRESENTATIONS_GET`.** `GOOGLESLIDES_PRESENTATIONS_GET` should be called with the Google Slides presentation ID. Get that ID from the presentation URL, or use the ID returned by `GOOGLEDRIVE_FIND_FILE` when discovering presentations through Drive. **Use the same Google account for discovery and reading.** When a workflow discovers presentations with `GOOGLEDRIVE_FIND_FILE` and then reads them with `GOOGLESLIDES_PRESENTATIONS_GET`, make sure the connected Google Drive and Google Slides accounts are the same account. Otherwise the ID may be valid in Drive discovery but inaccessible to the Slides connection. ## Create and connect Google Slides workflows [#create-and-connect-google-slides-workflows] **Create presentations through Google Super.** Google Slide creation tools were added to the Google Super toolkit. For slide creation workflows, use the relevant Google Super tools rather than trying to create a native Slides file through generic Drive text upload. **Verify custom OAuth apps for sensitive scopes.** When using a custom Google developer app for Google Slides, the app must be verified for the sensitive Google scopes it requests. Without verification, Google may block or warn on the OAuth consent flow. **Use the supported Google Slides trigger.** Google Slides is listed as a trigger-capable toolkit in Composio with one supported trigger. --- # Google Super (/kb/guide/toolkits-googlesuper) Use this guide to configure a Google Super connection and run Google Workspace actions with the required scopes and efficient filters. ## Configure Google Super access and consent [#configure-google-super-access-and-consent] **Use one connection across supported Google Workspace services.** Google Super is a unified/superset toolkit for Google Workspace services. It can cover tools across Gmail, Google Calendar, Google Meet, and related Google APIs through one Google Super connection when the required scopes are configured. **Remove unneeded scopes and tools carefully.** Google Super can cover all Google services including Gmail, but customers can remove scopes and tools they do not want as part of the Google Super auth/tool configuration. Make sure the remaining scopes still cover the tools the customer expects to use. **Treat a 10-minute initiation timeout as incomplete consent.** If expired connections share status reason `Connection initiation did not complete within 10 minutes`, the OAuth flow was initiated but the user did not complete consent within the 10-minute window. No provider tokens were issued in that case, so it is not a 1-2 week refresh token expiry problem. **Account for scopes users deselect during consent.** Google lets users selectively deselect scopes during consent. Composio marks the connection active as long as token exchange succeeds, even if the final granted scopes are a subset of the auth config's requested scopes. The auth config scopes are the blueprint, but the final permissions are decided by the end user on the consent screen. ## Enable service-specific scopes and APIs [#enable-service-specific-scopes-and-apis] **Configure Meet scopes and enable the Google Meet API.** To use Google Meet tools through Google Super, configure `https://www.googleapis.com/auth/meetings.space.created` and `https://www.googleapis.com/auth/meetings.space.settings` in the Google Super auth config, create a new connection for the scope changes to apply, and enable the Google Meet API in Google Cloud Console. **Include the Gmail settings scope for filter creation.** Google Super uses the same underlying Gmail API requirement for filter creation. See the canonical Gmail guidance: [Creating Gmail filters requires `gmail.settings.basic`](/kb/guide/toolkits-gmail). **Check spreadsheet identity, access, and scope when Sheets returns 404.** For Google Super Sheets 404s, first verify the spreadsheet ID, confirm the sheet is shared with the connected Google account, and ensure the connection has `https://www.googleapis.com/auth/spreadsheets`. If those are all correct and only one tool fails, contact Composio support with the redacted request/response payload and log ID. ## Query Gmail efficiently through Google Super [#query-gmail-efficiently-through-google-super] **Avoid label-detail fan-out when it is unnecessary.** For `GOOGLESUPER_LIST_LABELS`, setting `include_details=true` fans out into one Gmail API call per label. Accounts with many labels can become slow because the calls happen sequentially. Set `include_details=false` or omit the parameter to return to a single API call and much lower latency. **Use the thread result estimate from current versions.** The current Gmail thread-listing response includes `resultSizeEstimate`. If it is absent through an older pinned Google Super toolkit version, compare its schema with the latest version before changing application logic. **Filter messages with Gmail queries and label IDs.** Gmail/Google Super tools are wrappers over Google APIs, so use Gmail-style `query` filters or `label_ids` where supported to filter messages, including sent-mail style queries. If the exact filter is not exposed, submit the endpoint or parameter through the Composio request portal. --- # Google Tasks (/kb/guide/toolkits-googletasks) Use this guide to choose managed or customer-owned Google OAuth for Google Tasks and troubleshoot consent, scope, or token failures. ## Configure Google OAuth for Google Tasks [#configure-google-oauth-for-google-tasks] **Follow the Google Apps credential setup guide for custom OAuth.** For a step-by-step guide to creating and configuring Google OAuth credentials with Composio, see [How to create OAuth2 credentials for Google Apps](https://composio.dev/auth/googleapps). **Enable the Google Tasks API for custom OAuth.** Enable the Google Tasks API in the Google Cloud project that owns the credentials. After enabling it under **APIs & Services**, wait a few minutes and retry. **Choose managed or customer-owned OAuth.** Use Composio-managed OAuth for the standard connection flow. Use a custom Google OAuth app when you need control over scopes, consent-screen branding, or Google Cloud project policy. For custom OAuth, configure the app name and branding in that project and use the redirect URL shown by Composio's current auth-config flow. ## Troubleshoot Google Tasks OAuth and tool calls [#troubleshoot-google-tasks-oauth-and-tool-calls] **Remove unverified scopes when Google reports “App is blocked.”** This error usually means the OAuth client is requesting scopes that Google has not verified for that client. Remove additional scopes beyond the defaults, or use a custom OAuth app and submit the scopes for verification. **Validate scopes when OAuth returns `Error 400: invalid_scope`.** Verify the requested scopes and their formatting against the [Google OAuth scopes documentation](https://developers.google.com/identity/protocols/oauth2). **Reconnect when tool calls return 401.** A 401 usually means the access token is no longer valid. Re-authenticate the connected account and retry. --- # Granola MCP (/kb/guide/toolkits-granola-mcp) ## Composio mirrors Granola's official MCP server [#composio-mirrors-granolas-official-mcp-server] The Granola MCP toolkit uses Granola's official MCP server. Tool names, descriptions, input definitions, and response metadata are limited to what that upstream server exposes. * If Granola supplies only a tool name and description, that is the metadata Composio can expose. * If Granola does not declare a response/output schema, Composio cannot invent one, so an empty output schema is not by itself evidence of a stale Composio catalog. * If you find a mismatch, note the exact tool name and missing field. Compare it with the current official Granola MCP server behavior before attributing it to the Composio catalog. * If the official server currently exposes a tool or schema field but the same item is absent from Composio, contact Composio support and include those comparison details. --- # HubSpot (/kb/guide/toolkits-hubspot) Use this guide to configure HubSpot authentication, troubleshoot OAuth connections, call HubSpot APIs, and set up triggers. ## Configure HubSpot OAuth scopes and branding [#configure-hubspot-oauth-scopes-and-branding] **Choose the required contact scopes.** For HubSpot CRM contacts, the minimum scopes are `crm.objects.contacts.read` and `crm.objects.contacts.write`. Sensitive contact fields require the corresponding sensitive scopes such as `crm.objects.contacts.sensitive.read` and `.write`. **Map tools to scopes before configuring the app.** Use HubSpot's own scopes documentation and Composio's scopes/tools API to map actions to required scopes. This is better than guessing scopes manually. **Keep the HubSpot app and Composio auth config aligned.** HubSpot requires scopes to be declared in the app configuration before OAuth. The scope set on the Composio auth config should match the HubSpot app settings; HubSpot will not dynamically adjust scopes at connection time. **Use customer-owned credentials for white-label OAuth.** Use your own HubSpot OAuth app credentials/custom auth config. That gives control over branding/consent and avoids relying on the Composio managed app for the customer-facing OAuth screen. ## Troubleshoot HubSpot OAuth connections [#troubleshoot-hubspot-oauth-connections] **For a 400 during token exchange, check the client secret first.** Several reported customer-owned HubSpot OAuth failures were resolved by copying the correct current client secret from the HubSpot app and updating the Composio custom auth config to match. If the secret was rotated or copied from the wrong HubSpot app, HubSpot can fail token exchange with a 400. Then check scope alignment. HubSpot is strict about required scopes: * Required scopes configured on the HubSpot app must be present in the OAuth request/install URL `scope` parameter for successful installation. * If the Composio auth config requests required scopes that do not match the customer-owned HubSpot app's configured required scopes, authorization/token exchange can fail. * Optional scopes should be requested through HubSpot's `optional_scope` parameter. If the selected HubSpot account/user cannot grant an optional scope, HubSpot can omit it and the resulting token will not include that scope. Do not assume optional scopes were granted; inspect token/granted scopes before relying on optional capabilities. For Composio-managed HubSpot auth configs, do not change the default scope set. If you need a different required/optional scope configuration, use your own HubSpot OAuth app through a custom Composio auth config. **For an authorization loop, verify HubSpot's workspace and login state.** If the HubSpot flow loops while Composio works on its side, retry while logged into the correct HubSpot workspace and confirm the OAuth app is public/configured correctly. **To disconnect HubSpot, delete the connected account.** Deleting the connected account disconnects the HubSpot account from Composio and stops refreshing that access token. ## Use HubSpot APIs and current toolkit versions [#use-hubspot-apis-and-current-toolkit-versions] **Create custom HubSpot tools through authenticated API requests.** You can create a custom tool that sends authenticated requests to HubSpot API endpoints; Composio handles authentication for the connected account. Alternatively, call the provider directly with connection config/custom headers if needed. **Handle marketing objects separately from CRM properties.** For HubSpot marketing objects such as campaigns, HubSpot does not expose a properties API in the same way it does for CRM objects. You may need to inspect or configure these from the HubSpot portal. **Upgrade old HubSpot SDK and toolkit versions.** Older versions used slugs like `HUBSPOT_HUBSPOT_LIST_CONTACTS`; newer versions use slugs like `HUBSPOT_LIST_CONTACTS`. Update the SDK and explicitly use the latest HubSpot toolkit version. ## Configure HubSpot triggers for each customer app [#configure-hubspot-triggers-for-each-customer-app] HubSpot webhook APIs need the specific HubSpot app that should receive webhook notifications. Get the app ID from HubSpot's webhook app documentation or developer app settings and use it when configuring triggers. For triggers that use a customer-owned HubSpot app, `app_id` and developer API key are required because each app receives its own webhook delivery. --- # Instagram (/kb/guide/toolkits-instagram) ## Instagram OAuth tokens are bound to the account selected in Facebook Login [#instagram-oauth-tokens-are-bound-to-the-account-selected-in-facebook-login] Instagram connection goes through Facebook Login, where the user selects which Instagram accounts and Facebook Pages to grant access to. Once Instagram issues the token, it is bound to the specific account selected in that OAuth flow. Composio cannot repoint that token server-side to another Instagram account. To switch accounts, reconnect and select the intended Instagram account/page in the Facebook picker. ## Instagram uses Business Login and only supported/verified scopes should be configured [#instagram-uses-business-login-and-only-supportedverified-scopes-should-be-configured] The Instagram toolkit uses Instagram API with Business Login for Instagram. OAuth errors commonly happen when unsupported or unverified scopes are configured. Prefer the default scopes where possible, because they are intended to cover the toolkit's supported actions. If configuring custom scopes, use only Meta-supported Instagram Business Login permissions and remove unsupported scopes such as `user_profile`. ## Instagram toolkit requires a Business/Creator account for supported business features [#instagram-toolkit-requires-a-businesscreator-account-for-supported-business-features] Instagram toolkit support is for Instagram Business/Creator account flows. If you are using a personal Instagram account, convert or connect a Business/Creator account linked through Meta/Facebook as required by Instagram's API. ## Use `INSTAGRAM_LIST_ALL_MESSAGES` to fetch Instagram messages [#use-instagram_list_all_messages-to-fetch-instagram-messages] Use `INSTAGRAM_LIST_ALL_MESSAGES` to list Instagram messages. In playground, select the correct auth config/connected account; if the desired connected account does not appear, initiate a new connection for the test account and use that auth config. ## Instagram DM send failures with code 10/subcode 2534022 are Meta's 24-hour messaging window [#instagram-dm-send-failures-with-code-10subcode-2534022-are-metas-24-hour-messaging-window] That error is enforced by Instagram/Meta, not Composio. Instagram's messaging API only allows replies inside the 24-hour messaging window. Meta opens that window for specific interactions such as a direct DM from the user, story reply, story mention, or icebreaker/quick-reply button tap. Likes, comments, and follows do not open the window. If the qualifying interaction is older than 24 hours or never happened, the send will fail. If you have a fresh qualifying inbound DM, an accepted message request, the correct Business/Creator account, and a successful `INSTAGRAM_LIST_ALL_MESSAGES` call, the generic 24-hour-window explanation is not sufficient. Contact Composio support with the redacted call details for further investigation. * The current `INSTAGRAM_SEND_TEXT_MESSAGE` action sends `messaging_type: "RESPONSE"` for a normal in-window reply. * `INSTAGRAM_MARK_SEEN` can also return the same Meta subcode. Because sender actions are more provider-limited, retest the action before assuming it is supported for every Instagram account. If it still fails, contact Composio support with the exact request or log ID. ## For custom Instagram/Meta OAuth, configure the redirect URI in the Meta app [#for-custom-instagrammeta-oauth-configure-the-redirect-uri-in-the-meta-app] For custom Meta/Instagram OAuth apps, make sure the redirect URI is added in the correct Meta app configuration field and matches the Composio auth config redirect URI. Customers using their own auth app credentials can configure their own redirect URI. ## For Instagram DMs via n8n/Claude, Connect MCP can simplify setup [#for-instagram-dms-via-n8nclaude-connect-mcp-can-simplify-setup] For Instagram DM workflows in MCP clients, use Connect MCP at `https://connect.composio.dev/mcp` with the `x-consumer-api-key` header copied from the current AI Clients setup in the Composio dashboard. The agent can then start the Instagram connection flow when authentication is needed. ## `INSTAGRAM_POST_IG_MEDIA_COMMENTS` failures can be caused by an incorrect `ig_media_id` [#instagram_post_ig_media_comments-failures-can-be-caused-by-an-incorrect-ig_media_id] If `INSTAGRAM_POST_IG_MEDIA_COMMENTS` fails, verify the `ig_media_id` being passed. An incorrect media ID can cause the action to fail even when the action itself is available. ## Instagram is available as a toolkit and can be connected via a new authConfig [#instagram-is-available-as-a-toolkit-and-can-be-connected-via-a-new-authconfig] Instagram is available in the Composio marketplace. Create a new Instagram authConfig, complete the OAuth connection for the Instagram account, and then use the Instagram toolkit tools. The authConfig ID / integration ID can be found from the dashboard. ## Publish local media with `image_file` or `video_file` [#publish-local-media-with-image_file-or-video_file] For a locally generated JPEG, PNG, or video, use `INSTAGRAM_POST_IG_USER_MEDIA` and pass the staged file through `image_file` or `video_file`. Upload or stage the file first; a raw local path, workspace/session path, or stale storage key can fail before Meta receives the request. Follow with `INSTAGRAM_POST_IG_USER_MEDIA_PUBLISH` when the create step succeeds. Alternatively, use `image_url` or `video_url` only when it is a direct HTTP(S) media URL that Meta can fetch without authentication. The older `INSTAGRAM_CREATE_MEDIA_CONTAINER` path is URL-only and does not accept local files directly. If the error says `Failed to download file with s3key ... storage returned HTTP 404`, re-stage the file and retry with the fresh `FileUploadable` object. Treat this as a Composio file-reference failure before provider execution, not an Instagram OAuth failure. --- # Intercom (/kb/guide/toolkits-intercom) ## Use External Pages or a custom agent when connecting MCP knowledge to Intercom Fin [#use-external-pages-or-a-custom-agent-when-connecting-mcp-knowledge-to-intercom-fin] Composio does not control how Intercom Fin retrieves knowledge inside Intercom. For this use case, either push MCP-derived content into Fin's Content Library by creating and managing Intercom External Pages, or build a custom AI agent with Composio SDKs that connects to both the MCP server and Intercom for support workflows such as replying to conversations, creating tickets, and managing contacts. ## INTERCOM\_LIST\_ALL\_COMPANIES per\_page limit is 60 [#intercom_list_all_companies-per_page-limit-is-60] For Intercom company listing through Composio, keep `per_page` at 60 or lower. The generic Intercom pagination page can be misleading for this endpoint; Composio verified the list companies endpoint limit as 60 and updated the field description accordingly. ## Update Python SDK packages when Intercom tool schemas fail on reserved parameter names [#update-python-sdk-packages-when-intercom-tool-schemas-fail-on-reserved-parameter-names] This reserved-keyword schema issue was fixed in the SDK. Ask the user to update both `composio` and `composio-langchain` to the latest available versions; the fix was available by SDK version `0.11.4`. --- # Jira (/kb/guide/toolkits-jira) ## Keep Jira OAuth scopes within Atlassian's supported set [#keep-jira-oauth-scopes-within-atlassians-supported-set] Jira/Atlassian limits an OAuth app to 50 scopes, and unsupported or mismatched scopes can make consent fail. For a customer-owned app, keep the auth config aligned with the scopes approved on that Atlassian app. Diagnose current managed-auth failures from the current consent error and auth config rather than from previously resolved scope behavior. ## Pin custom Jira authConfig when creating Tool Router sessions [#pin-custom-jira-authconfig-when-creating-tool-router-sessions] When using a custom Jira OAuth app with Tool Router, pass the custom auth config while creating the session. If the session does not specify the Jira auth config, Tool Router can fall back to an auto-generated/default Jira config and fail to see the customer's active custom-auth connections. Pin the active BYOA config, for example `auth_configs: { jira: "" }`, so Tool Router resolves the intended Jira connected accounts. ## Jira custom token execution needs the Atlassian base URL/subdomain [#jira-custom-token-execution-needs-the-atlassian-base-urlsubdomain] Jira expects the tenant URL in the form `https://.atlassian.net`. Supply the `subdomain` when initiating the connected account for OAuth2, API-key, or S2S OAuth2 auth. `JIRA_GET_SERVER_INFO` can help confirm the base URL. Do not rely on the old SDK workaround that injected a raw access token through `customConnectionData`. ## Jira search pagination tokens returned by current tools preserve search context [#jira-search-pagination-tokens-returned-by-current-tools-preserve-search-context] Current Jira search tools wrap provider pagination tokens with the original search context. Pass the `next_page_token` returned by the same Composio action directly to its next call. If a caller instead supplies a raw Jira `nextPageToken`, it must also supply the original JQL. Workaround: * Do not pass a token returned by one Jira action to a different action. * Use the token immediately for the next page. * Do not persist old tokens or retry rejected tokens. If Jira returns `invalid or expired` even with the same original context, discard the token and restart pagination from page 1. ## Jira OAuth redirect URI must match the authConfig and Atlassian app [#jira-oauth-redirect-uri-must-match-the-authconfig-and-atlassian-app] For Jira/Atlassian OAuth, configure the same redirect URI in both the Composio auth config and the Atlassian OAuth app. Copy the callback shown by the current auth-config flow or documentation and match it exactly. Do not reuse legacy v1 or v3 callback paths from older examples. ## Missing `audience=api.atlassian.com` can prevent Jira refresh tokens [#missing-audienceapiatlassiancom-can-prevent-jira-refresh-tokens] Atlassian OAuth 2.0 requires `audience=api.atlassian.com` in the authorization URL. Without this parameter, Atlassian may not honor `offline_access`, meaning no refresh token is returned and the access token expires without being refreshable. If Jira credentials expire immediately, check whether the connected account is missing `offline_access` and whether the Jira OAuth config includes the required `audience` parameter. As an urgent workaround, API key auth with Atlassian email + API token can provide stable non-expiring credentials. ## Use `JIRA_GET_CREATE_METADATA_ISSUE_TYPE_FIELDS` instead of deprecated create metadata behavior [#use-jira_get_create_metadata_issue_type_fields-instead-of-deprecated-create-metadata-behavior] Use `JIRA_GET_CREATE_METADATA_ISSUE_TYPE_FIELDS` for the closest replacement behavior to the deprecated `JIRA_GET_ISSUE_CREATE_METADATA` flow. The replacement was added after Jira deprecated the older create-metadata API behavior. ## Download Jira attachments with `JIRA_GET_ATTACHMENT` [#download-jira-attachments-with-jira_get_attachment] Use `JIRA_GET_ATTACHMENT` to retrieve the binary content of a Jira attachment by attachment ID. This tool is intended for downloading a specific file attached to a Jira issue. ## Jira tool-call payload retention follows the project log-storage setting [#jira-tool-call-payload-retention-follows-the-project-log-storage-setting] Composio manages Jira OAuth tokens and returns Jira API responses to the customer's application. Whether request and response payloads are retained in Composio tool logs follows the project's log-storage setting; **Don't store data** omits payload content from new log rows but preserves audit metadata. The customer's own agent or application may retain tool outputs separately. ## Jira service account use requires customer-owned credentials and scopes [#jira-service-account-use-requires-customer-owned-credentials-and-scopes] For Jira service-account-style usage, customers should use their own credentials with the required Jira and Jira service-account scopes when no dedicated managed auth app is available for that flow. --- # Kickbox (/kb/guide/toolkits-kickbox) ## Single verification auth and EU endpoint checks [#single-verification-auth-and-eu-endpoint-checks] For `KICKBOX_SINGLE_VERIFICATION_API`, the Composio credential field is `generic_api_key`. For direct/custom credential execution, customers should pass: ```json { "val": { "generic_api_key": "" } } ``` Do not assume `api_key` is the correct field name for Composio custom credential data. The Kickbox provider API itself documents an `apikey` query parameter, but Kickbox's official quickstart also says `Authorization: Bearer ` is accepted. Composio currently uses the Bearer header and `https://api.kickbox.com/v2/verify`, which is valid for standard Kickbox accounts. If Kickbox returns 403 `Invalid API key`, verify: * the redacted `custom_connection_data.val` shape * key validity * key permissions * account/credit state * whether the Kickbox account is EU-only Kickbox docs say EU-only accounts that sign in from `app.eu.kickbox.com` must use `api.eu.kickbox.com`. If your account is EU-only, contact Composio support about a possible toolkit base-URL/region gap because the current toolkit uses the standard `api.kickbox.com` host. Useful source docs: * Single Verification API: [https://docs.kickbox.com/docs/single-verification-api](https://docs.kickbox.com/docs/single-verification-api) * API Quickstart / authentication and EU endpoint note: [https://docs.kickbox.com/docs/using-the-api](https://docs.kickbox.com/docs/using-the-api) --- # Klaviyo (/kb/guide/toolkits-klaviyo) ## Klaviyo schema keys that exceeded Claude's 64-character limit were fixed [#klaviyo-schema-keys-that-exceeded-claudes-64-character-limit-were-fixed] For Klaviyo tool schemas that failed Claude validation because flattened nested property keys exceeded 64 characters, the backend schema-generation issue was fixed in the latest version. Update or re-fetch the latest tools/schema before retrying. The same fix also addressed top-level parameter naming issues such as `$` prefixes; nested `$` parameters were verified as accepted across major model providers and SDKs. --- # Kommo (/kb/guide/toolkits-kommo) ## Enter only the Kommo account subdomain [#enter-only-the-kommo-account-subdomain] The **Subdomain** field should contain only the part before `.kommo.com` in the account URL. For `https://yourcompany.kommo.com`, enter `yourcompany`, not an email domain or a value containing `.com` or dots. If a failed connection already exists, delete it, reconnect, and enter the corrected subdomain. --- # LaunchDarkly (/kb/guide/toolkits-launch-darkly) ## LaunchDarkly currently uses a REST API access token [#launchdarkly-currently-uses-a-rest-api-access-token] LaunchDarkly in Composio currently uses a LaunchDarkly REST API access token. ## OAuth client actions operate after toolkit authentication [#oauth-client-actions-operate-after-toolkit-authentication] The LaunchDarkly toolkit includes actions such as `Create OAuth 2.0 Client`. These call LaunchDarkly endpoints like `POST /oauth/clients` after the toolkit is already authenticated with an API access token. Use this distinction when replying to customers: * The action can create or manage a LaunchDarkly OAuth client inside LaunchDarkly. * The current toolkit connection still uses the LaunchDarkly REST API access token described above. --- # Linear (/kb/guide/toolkits-linear) ## Linear triggers require a valid `team_id` [#linear-triggers-require-a-valid-team_id] `team_id` is required for Linear triggers. An invalid-input error during trigger or webhook creation usually means the supplied team ID is missing or invalid. Use `LINEAR_LIST_LINEAR_TEAMS` to retrieve valid team IDs, then pass the selected team ID into the trigger configuration. --- # LinkedIn (/kb/guide/toolkits-linkedin) ## Fix LinkedIn 426 NONEXISTENT\_VERSION by using the latest toolkit version [#fix-linkedin-426-nonexistent_version-by-using-the-latest-toolkit-version] LinkedIn 426 `NONEXISTENT_VERSION` errors usually mean the request is using an older LinkedIn API version header. In Composio, this often happens when calls run on the base toolkit version `00000000_00` or another older pinned version. Specify the latest LinkedIn toolkit version on tool calls, or pin to the current fixed version if needed. If the error persists after switching to the latest version, contact Composio support with a failed call `logId` or request ID so the actual `LinkedIn-Version` header can be verified. ## Fetch modern LinkedIn tools with `toolkit_slug=linkedin` and `toolkit_versions=latest` [#fetch-modern-linkedin-tools-with-toolkit_sluglinkedin-and-toolkit_versionslatest] The v3 tools-list endpoint defaults to the base toolkit version when no toolkit version is specified, which can return only legacy LinkedIn slugs. Use the singular filter `toolkit_slug=linkedin`; plural or alternate filters such as `toolkit_slugs`, `toolkits`, `app`, or `app_names` may be ignored. Add `toolkit_versions=latest`. Example: `GET /api/v3/tools?toolkit_slug=linkedin&toolkit_versions=latest&limit=100`. ## LinkedIn organization scopes depend on the toolkit and auth config [#linkedin-organization-scopes-depend-on-the-toolkit-and-auth-config] An active LinkedIn connection can run personal/profile actions while organization actions return 403. Check the actual toolkit and scopes stored on the auth config: the standard LinkedIn flow commonly uses personal scopes, while LinkedIn Ads can request organization and advertising scopes. For organization ACLs, page statistics, or company-page posting, use an auth config that explicitly requests the required organization scopes and reconnect so LinkedIn issues a new grant. Reconnecting an unchanged config does not add scopes. Do not assume provider approval alone means those scopes were requested by the concrete connection. ## LinkedIn post creation supports image arrays through SDK/API [#linkedin-post-creation-supports-image-arrays-through-sdkapi] `LINKEDIN_CREATE_LINKED_IN_POST` supports image + text posting, including multiple images when using SDKs or APIs directly. Pass an array of values to the `images` field. If image posting fails, first confirm you are using a recent toolkit version, then contact Composio support with log IDs from failed tool calls if needed. ## Use Connect MCP instead of legacy Platform MCP for consumer LinkedIn connector flows [#use-connect-mcp-instead-of-legacy-platform-mcp-for-consumer-linkedin-connector-flows] For consumer/client connector flows, use `connect.composio.dev` / Connect MCP rather than the legacy Platform MCP endpoint. The API key does not belong in the URL; configure the `x-consumer-api-key` header shown by the current AI Clients setup. If LinkedIn MCP calls fail with 401 despite an active connection, confirm the endpoint and header type. If the error persists, contact Composio support with the exact error and log ID. ## Fix LinkedIn Ads `redirect_uri` mismatch before debugging scopes [#fix-linkedin-ads-redirect_uri-mismatch-before-debugging-scopes] If LinkedIn rejects authorization with `The redirect_uri does not match the registered value`, register the exact callback shown by the current Composio auth-config flow in the customer's LinkedIn developer app. Do not guess between legacy v1, v3, and v3.1 callback paths; copy the callback from the current setup UI or auth-config documentation and match it exactly, without adding a trailing slash. This error occurs before a successful callback and is separate from LinkedIn product or scope approval. ## LinkedIn Ads `unauthorized_scope_error` means a requested scope is unavailable [#linkedin-ads-unauthorized_scope_error-means-a-requested-scope-is-unavailable] LinkedIn rejects the complete OAuth request when any requested scope is unavailable to the developer app. Compare the exact auth-config scope set with the products and scopes enabled on that same LinkedIn app. The default LinkedIn Ads flow includes OpenID Connect scopes (`openid`, `profile`, `email`) as well as advertising and organization scopes. Legacy `r_basicprofile` is not a substitute for the OpenID Connect scopes. Enable the relevant LinkedIn products or narrow a custom auth config to scopes the app actually has, then reconnect. --- # Mailchimp (/kb/guide/toolkits-mailchimp) ## Mailchimp server prefix is the URL prefix before .admin.mailchimp.com [#mailchimp-server-prefix-is-the-url-prefix-before-adminmailchimpcom] When connecting Mailchimp, pass the correct server prefix. It is the part of the Mailchimp URL before `.admin.mailchimp.com`. For example, if the Mailchimp URL is `https://us19.admin.mailchimp.com/`, the server prefix is `us19`. A wrong prefix can cause Mailchimp API calls to fail even if the API key/token itself looks correct. ## Use subdomain or dc, not server\_prefix, for Mailchimp connectionConfig [#use-subdomain-or-dc-not-server_prefix-for-mailchimp-connectionconfig] For Mailchimp API connection configuration, send `connectionConfig.subdomain` with the server prefix value, or use the legacy alias `dc`. Do not send `server_prefix`; that key is ignored by the validator and may fall back to a default such as `us21`. The UI label may say Server Prefix, but the API field name is `subdomain`. ## Some Mailchimp tools require at least the Mailchimp Essentials plan [#some-mailchimp-tools-require-at-least-the-mailchimp-essentials-plan] If Mailchimp tools fail despite the connection looking correct, check the customer's Mailchimp plan. Some Mailchimp API/tool capabilities require at least the Mailchimp Essentials plan. A free Mailchimp account may not be enough for the requested tool flow. ## Mailchimp Proxy Execute with raw access token also needs subdomain [#mailchimp-proxy-execute-with-raw-access-token-also-needs-subdomain] When using Proxy Execute with Mailchimp custom connection data, include both the OAuth access token and Mailchimp `subdomain`/server prefix. The endpoint can then be called through `/api/v3/tools/execute/proxy` with `toolkitSlug: "mailchimp"`, `authScheme: "OAUTH2"`, and `val` containing `access_token` plus `subdomain` such as `us20`. ## Mailchimp has trigger support in the supported-trigger toolkit list [#mailchimp-has-trigger-support-in-the-supported-trigger-toolkit-list] Mailchimp appears in the supported-trigger toolkit list. Before using a specific Mailchimp trigger, verify that the exact trigger or event exists in the current toolkit. If it does not, submit the use case through the Composio request portal. --- # Marketstack (/kb/guide/toolkits-marketstack) ## API Coverage [#api-coverage] Marketstack's official APILayer v2 OpenAPI spec includes live and intraday market data endpoints that are not yet exposed by the current Composio Marketstack toolkit actions: * `/stockprice` * `/intraday` * `/intraday/latest` * `/intraday/{date}` * `/tickers/{symbol}/intraday` * `/tickers/{symbol}/intraday/latest` * `/exchanges/{mic}/intraday` * `/exchanges/{mic}/intraday/latest` * `/exchanges/{mic}/intraday/{date}` Supported intraday intervals in the OpenAPI spec are `1min`, `5min`, `10min`, `15min`, `30min`, and `1hour`. Intraday docs note that some TOPS feed fields can be null without IEX entitlement, while derived intraday data is available without an additional IEX market data agreement. Current Composio toolkit coverage includes EOD, ticker EOD, ticker EOD latest, ticker info/listing, exchange info/listing, splits, dividends, and currencies. Live quotes and 1D charts are not currently exposed as Marketstack toolkit actions; submit those capabilities through the Composio request portal rather than treating them as provider limitations. Do not promise Marketstack support for gainers, losers, most-active, movers, or sector-performance endpoints based on current v2 docs. Those paths are not present in the official OpenAPI spec as of 2026-06-21. --- # Microsoft Teams (/kb/guide/toolkits-microsoft-teams) Use this guide to configure Microsoft Teams scopes, fix stale OAuth metadata, connect through MCP, and troubleshoot chat or tool execution. ## Configure Microsoft Teams scopes and Azure consent [#configure-microsoft-teams-scopes-and-azure-consent] **Check delegated permissions against the latest tool version.** For Microsoft Teams scope checks, call `/api/v3/tools/get_scopes_required` with the exact tool slug and include `toolkit_versions[microsoft_teams]=latest` when needed. Without the explicit toolkit version, the API may return data from the old `00000000_00` version. **Use the delegated scopes required by each action.** `MICROSOFT_TEAMS_CHATS_GET_ALL_CHATS` can use `Chat.ReadBasic`, `Chat.Read`, or `Chat.ReadWrite`. `MICROSOFT_TEAMS_CREATE_MEETING` requires `OnlineMeetings.ReadWrite`. Confirm exact required scopes with the latest versioned scope endpoint before changing auth config scopes. **Use customer-owned Azure credentials when custom scopes are needed.** For Microsoft Teams, recommend using the customer's own Azure/Microsoft developer app credentials when custom scopes are needed. Additional scopes should be added in the Microsoft app, and admin consent may need to be granted in Azure before the connection has usable permissions. ## Fix the invalid OAuth scope `ChannelMessage.Read.Group` [#fix-the-invalid-oauth-scope-channelmessagereadgroup] If Microsoft Teams OAuth fails before consent with: ```text AADSTS650053: The application asked for scope 'ChannelMessage.Read.Group' that doesn't exist on the resource Microsoft Graph. ``` treat `ChannelMessage.Read.Group` as the wrong auth layer for the Composio delegated OAuth flow. It is a Microsoft Teams resource-specific consent (RSC) permission, not a normal Microsoft Graph OAuth scope to include in an OAuth `/authorize` URL. Debug steps: * Check whether the customer is using v3 base/default tool metadata or old Teams slugs. * `MICROSOFT_TEAMS_TEAMS_GET_MESSAGE` on v3 base can return `ChannelMessage.Read.Group`; the latest/v3.1 replacement is `MICROSOFT_TEAMS_GET_CHANNEL_MESSAGE`. * Use v3.1 or pass `toolkit_versions[microsoft_teams]=latest` when fetching tools/scopes. * Remove `ChannelMessage.Read.Group` from the OAuth auth config scopes and use `ChannelMessage.Read.All`, `Group.Read.All`, or `Group.ReadWrite.All` according to the latest tool scope response. * If an existing auth config already includes the invalid scope, update or recreate it and reconnect. Existing connected accounts may need refresh/reconnect depending on how the customer propagates scope changes. Minimal stale-path repro: ```bash curl --globoff 'https://backend.composio.dev/api/v3/tools/MICROSOFT_TEAMS_TEAMS_GET_MESSAGE' \ -H 'x-api-key: ' ``` Expected stale response includes `version: "00000000_00"` and `scopes: ["ChannelMessage.Read.Group"]`. Clean path: ```bash curl --globoff 'https://backend.composio.dev/api/v3.1/tools/MICROSOFT_TEAMS_GET_CHANNEL_MESSAGE' \ -H 'x-api-key: ' ``` Expected clean response includes `ChannelMessage.Read.All`, not `ChannelMessage.Read.Group`. ## Connect Teams through MCP and Tool Router [#connect-teams-through-mcp-and-tool-router] **Match the MCP URL user ID to the connected account.** For Microsoft Teams MCP, the user ID in the MCP server URL/query params must match the user ID attached to the connected account. If the connection is bound to an email/GUID, use that value in the MCP URL or create a new server/connection with the desired user ID. **Pass Tool Router memory as a list under the toolkit key.** When passing Tool Router memory for Microsoft Teams, use a real list under the `microsoft_teams` key, for example `"memory": { "microsoft_teams": ["Session id..."] }`. Do not pass escaped square brackets as a string. ## Create chats and troubleshoot tool execution [#create-chats-and-troubleshoot-tool-execution] **Pass two users with the correct OData bind format for one-on-one chats.** For Microsoft Teams one-on-one chat creation, pass two users, not one. Also make sure the OData bind payload uses the correct role and bind-data format expected by Microsoft Graph. **Validate user IDs and chat membership for 400/403/404 errors.** For `MICROSOFT_TEAMS_LIST_USER_CHAT_MESSAGES`, a 400 commonly means `user_id` was not passed as a GUID or UPN. For chat members tools, 403/404 often means the connected user is not part of the meeting chat or the chat ID is not in that user's scope. Use `MICROSOFT_TEAMS_LIST_USERS` to find valid user IDs and verify the connected user is a participant in the target chat. **Increase `limit` when the tool list stops at 20.** When fetching Microsoft Teams tools by toolkit, the default list may return only 20 tools. Increase the `limit` parameter or search for exact tool slugs to retrieve the full set. **Prefer replacement slugs over restored deprecated aliases.** Some old Microsoft Teams slugs were deleted during cleanup and then restored with a deprecated flag and descriptions pointing to the correct replacement slugs. If a Teams slug suddenly disappears or changes, check the latest toolkit version/changelog and prefer the replacement slug. --- # Monday (/kb/guide/toolkits-monday) ## Monday requires the OAuth app to be installed in the workspace before user connections [#monday-requires-the-oauth-app-to-be-installed-in-the-workspace-before-user-connections] Monday is unusual among popular toolkits because the OAuth app must be installed in the Monday workspace before users initiate individual OAuth connections. An admin can install the app once for the workspace, then users can connect normally. For Composio's managed Monday app, use the official installation control or link exposed by the current connection flow. Do not construct or share a raw OAuth URL with a hard-coded client ID. ## Add the Composio redirect URL to the Monday OAuth app [#add-the-composio-redirect-url-to-the-monday-oauth-app] For a custom Monday OAuth app, add the Composio redirect URL/callback URL to the Monday app settings. After the OAuth flow completes, the access token is populated by Composio automatically. ## `MONDAY_UPDATE_ITEM` body must be passed as a properly escaped string [#monday_update_item-body-must-be-passed-as-a-properly-escaped-string] `MONDAY_UPDATE_ITEM` expects the body in a format Monday's API accepts. If you pass JSON-like text or strings containing quotes/special characters, escape those characters and send a suitable string rather than unsupported raw structured content. ## Tool Router may prefer `MONDAY_MCP` over `MONDAY` when both are available [#tool-router-may-prefer-monday_mcp-over-monday-when-both-are-available] If both `MONDAY` and `MONDAY_MCP` are enabled, Tool Router may choose `MONDAY_MCP` for search/execution. If you specifically need the regular Monday toolkit, disable `monday_mcp` in the session or narrow toolkit availability so `COMPOSIO_SEARCH_TOOLS` returns the intended tools. ## Monday scopes come from the OAuth app and do not need separate Composio-side setup in the common flow [#monday-scopes-come-from-the-oauth-app-and-do-not-need-separate-composio-side-setup-in-the-common-flow] For Monday, the scopes configured on the Monday OAuth app are picked up during authorization. In the common flow, there is no separate Composio-side scope configuration required unless you intentionally request a subset. ## Monday trigger management is not handled by the agent at runtime [#monday-trigger-management-is-not-handled-by-the-agent-at-runtime] Trigger setup and management should be handled outside the agent runtime, for example through the CLI/API/dashboard flow. The agent should consume trigger payloads, not create or manage trigger instances as part of normal tool execution. --- # NetSuite (/kb/guide/toolkits-netsuite) ## NetSuite OAuth token exchange failures can be caused by generic OAuth endpoints [#netsuite-oauth-token-exchange-failures-can-be-caused-by-generic-oauth-endpoints] For NetSuite OAuth2 callback/token-exchange failures, verify whether the OAuth flow is using the customer's account-specific NetSuite authorize/token endpoint. NetSuite expects OAuth endpoints to be keyed to the NetSuite account subdomain; using a generic endpoint can produce a token-exchange failure that looks like a permissions or role problem. Check the decoded token-exchange response before advising the customer to change NetSuite roles. --- # Notion (/kb/guide/toolkits-notion) Use this guide to choose current Notion tools and triggers, understand integration access, and troubleshoot connection or response-size issues. ## Use current Notion tools and triggers [#use-current-notion-tools-and-triggers] **Retrieve pages with the current tool slug.** `NOTION_GET_PAGE` is not the current valid slug. Use `NOTION_RETRIEVE_PAGE`, and verify available Notion tools from the marketplace/tool listing. **Fetch Notion data with the current tool slug.** `NOTION_FETCH_NOTION_DATA` is not valid. Use `NOTION_FETCH_DATA` instead. **Choose the trigger that matches the Notion event.** The current Notion catalog includes separate triggers for page creation, page content updates, page property updates, and data-source schema updates. Choose the trigger that matches the event rather than expecting a page-created trigger to fire for edits. Fetch the current trigger catalog before implementation and use the exact returned slug. ## Configure Notion access and connected accounts [#configure-notion-access-and-connected-accounts] **Grant page and database access through the Notion integration.** Notion does not model access as normal OAuth scopes. Page/database access is granted per Notion integration/OAuth client ID through Notion “Capabilities” and workspace grants. If multiple Composio auth configs use the same underlying Notion integration, page authorization can overlap. **Treat auth config selection as connection lookup behavior.** Existing connected accounts under a different auth config continue to refresh and work. Specifying an auth config affects which connection get/use functions look for; it does not rewrite refresh behavior for already-valid connected accounts. ## Troubleshoot Notion connections and large responses [#troubleshoot-notion-connections-and-large-responses] **Check for a revoked integration when Notion returns 401 or refresh fails.** A Notion refresh failure with “Invalid refresh token” is usually a token revocation issue. Common causes are the user disconnecting the integration in Notion settings or a workspace admin removing/blocking the integration. **Keep Notion responses focused.** Large response payloads and overly complex structures can degrade agent behavior. Prefer narrower fetches/filters where available and track product improvements for simpler response structures. --- # Odoo (/kb/guide/toolkits-odoo) ## JSON-RPC access errors can arrive inside HTTP 200 responses [#json-rpc-access-errors-can-arrive-inside-http-200-responses] `ODOO_CALL_ODOO_JSONRPC` can receive HTTP 200 while the JSON-RPC body contains an `error` such as `access denied`. HTTP success only confirms transport; inspect the returned JSON-RPC envelope. Verify the instance URL, database name, API key, and the Odoo user's permission for the requested model and method. Prefer current JSON-2 tools where they cover the use case. If the body still contains an application error, share the Composio log ID and timestamp without sharing the API key. --- # OneDrive (/kb/guide/toolkits-one-drive) Use this guide to configure OneDrive OAuth, execute current file tools, and control Tool Router sessions safely. ## Configure OneDrive OAuth and scopes [#configure-onedrive-oauth-and-scopes] **Verify Azure app setup and recreate the auth config after changes.** For OneDrive custom OAuth failures, first verify the Azure OAuth app setup, especially credentials and redirect URLs. If the Azure app settings were changed, create a new Composio integration/auth config with the updated configuration and retry the connection. **Derive Microsoft Graph permissions from the tools.** For OneDrive and other Microsoft Graph-backed toolkits, use `/api/v3/tools/get_scopes_required` with the relevant tool slugs to determine the scopes needed by those tools. This is more reliable than manually guessing Microsoft Graph delegated permissions. ## Use current OneDrive tools and file inputs [#use-current-onedrive-tools-and-file-inputs] **Pass `version=latest` when folder behavior looks stale.** If OneDrive folder listing or related tool behavior appears stale, pass `version: "latest"` in the tool execution request so the call uses the latest toolkit version instead of the default pinned version. **Use supported upload inputs for file actions.** OneDrive has upload/update tools such as `ONE_DRIVE_ONEDRIVE_UPLOAD_FILE` and `ONE_DRIVE_UPDATE_FILE_CONTENT`. Where the selected action supports it, pass file content through `FileUploadable` or the shared storage/data-URI path, including base64-backed uploads. ## Configure Tool Router sessions and safety [#configure-tool-router-sessions-and-safety] **Keep connected accounts under the same `user_id`.** In Tool Router v2, connected accounts used in one session should belong to the same `user_id`. When creating the session, pass the intended auth config IDs and make sure the connected accounts for OneDrive and the other toolkits are associated with that same user. **Disable destructive actions with tags or exact tool slugs.** Use session-level tag controls to disable destructive tools globally or per toolkit. For OneDrive, disable the `destructiveHint` tag at the toolkit/session level, or disable exact tool slugs for finer-grained control. --- # Microsoft OneNote (/kb/guide/toolkits-onenote) ## OneNote uses a customer-owned Microsoft OAuth app [#onenote-uses-a-customer-owned-microsoft-oauth-app] The current `onenote` toolkit supports OAuth2 and requires a client ID and client secret from a Microsoft Entra app registration. Enter the app's Application (client) ID and the secret **value**, not the secret's identifier. Register the exact redirect URI shown by the current Composio auth-config flow. Choose the least-privileged delegated Microsoft Graph permissions that cover the intended OneNote actions. Common permissions include `Notes.Read`, `Notes.Create`, `Notes.ReadWrite`, and the corresponding `*.All` permissions for notebooks available through groups or sites. Include `offline_access` when the connection needs a refresh token. Microsoft documents the permission set in its [Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference#notesreadwrite). Tenant policy or higher-privilege permissions can require administrator consent. Follow the shared Microsoft OAuth guidance and create a fresh connection after changing the app's permissions. --- # OpenAI (/kb/guide/toolkits-openai) ## `OPENAI_CREATE_IMAGE` supports `gpt-image-2` in the latest toolkit version [#openai_create_image-supports-gpt-image-2-in-the-latest-toolkit-version] `gpt-image-2` has been shipped and can be used through `OPENAI_CREATE_IMAGE` on the latest toolkit version. If the model is missing, have the customer update the toolkit/tool version before retrying. ## Use `OpenAIAgentsProvider` when wiring Composio tools into OpenAI Agents [#use-openaiagentsprovider-when-wiring-composio-tools-into-openai-agents] For OpenAI Agents, initialize Composio with `OpenAIAgentsProvider`, create a session for the user, fetch tools from the session, and pass those tools into the OpenAI Agent. This is the expected provider path when using the OpenAI Agents SDK with Composio. ## Pin auth config and connected account IDs in Tool Router sessions when a specific connection must be used [#pin-auth-config-and-connected-account-ids-in-tool-router-sessions-when-a-specific-connection-must-be-used] When creating a Tool Router session, pass the desired `authConfigId` and `connectedAccountId` in the session creation options. Use `authConfigs: { [toolkitSlug]: authConfigId }` and `connectedAccounts: { [toolkitSlug]: connectedAccountId }` so the session uses that specific connection instead of relying on discovery/default selection. ## Use `beforeExecute` modifiers to add a human approval layer before tool execution [#use-beforeexecute-modifiers-to-add-a-human-approval-layer-before-tool-execution] Composio SDK modifiers can be used to add a gating layer before tool execution. Implement a `beforeExecute` modifier to inspect the tool call, request approval, and only allow the execution to continue when the customer's approval logic passes. ## Provider/schema compatibility errors often require upgrading Composio SDK packages together [#providerschema-compatibility-errors-often-require-upgrading-composio-sdk-packages-together] When debugging provider/schema errors with OpenAI or LangChain-style integrations, upgrade both the core Composio package and the relevant provider package to their latest compatible versions before retesting. --- # Outlook (/kb/guide/toolkits-outlook) Use this guide to authorize Outlook, resolve Microsoft tenant consent, and target the correct mailbox or account through MCP and direct execution. ## Connect and authorize Outlook [#connect-and-authorize-outlook] **Authenticate the cloud Microsoft account in a browser.** Outlook tools authenticate through the Microsoft account/OAuth flow in a browser. Even if you only use Outlook desktop, log into the underlying Microsoft/Outlook account in the browser to complete OAuth. Desktop and cloud use the same account, so once the account is authenticated, the tools can operate against that mailbox. **Check exact tool scopes, then reconnect after changes.** For Outlook 403s, look up required scopes with `/api/v3/tools/get_scopes_required` using the exact Outlook tool slug, not the toolkit name. For example `OUTLOOK_GET_MAILBOX_SETTINGS` requires `MailboxSettings.ReadWrite`. After adding scopes to the auth config, create a new auth link session and have the user reconnect so the new scopes are granted. **Complete auth links within about 10 minutes.** If a connected account expires because the initiation flow was not completed, the likely reason is that the authorization link timed out. Users have roughly a 10-minute window to complete the auth flow; otherwise Composio invalidates the link and marks the connected account `EXPIRED`. ## Grant Microsoft tenant admin consent [#grant-microsoft-tenant-admin-consent] Microsoft/Outlook admin-consent issues are Microsoft 365 tenant-level approval problems, not a Composio-side connection configuration issue. Adding delegated permissions to an Azure app registration is not the same as granting tenant admin consent. Once a tenant admin grants consent for the requested permissions, affected users should start a fresh normal Outlook connection flow with their own accounts; the admin does not need to connect every user individually. Two concrete ways an admin can approve: 1. **App Registration / OAuth app level:** in Microsoft Entra / Azure Portal, go to **App registrations**, open the OAuth app, go to **API permissions**, click **Grant admin consent for \[Tenant Name]**, then confirm/save. 2. **Enterprise Applications / org level:** in Microsoft Entra / Azure Portal, go to **Enterprise applications**, find the Composio/Outlook app or the customer's own service principal, open **Permissions** / admin-consent controls, then grant admin consent for the organization. For the Composio-managed Outlook app, Microsoft's in-flow `sign in as an admin` / `Connectez-vous avec ce compte` link is also a real tenant-admin consent path. If the admin signs in through that same OAuth attempt, that attempt may connect the admin's mailbox, not the original user's mailbox; treat that connected account as the admin's and have the original user start a fresh Connect flow afterward. Incomplete/pending Outlook connection attempts expire after about 10 minutes, so an expired non-admin attempt cannot be resumed. Nothing needs to happen on Composio's side between the admin grant and the user's retry: no cache clear, webhook, or manual status change. Composio does not publish a `client_id` for its managed Outlook app for use in direct Microsoft `adminconsent` URLs. Do not guess this value. For a customer-owned/BYOA Azure app, use your own app's `client_id` and tenant ID in Microsoft's admin-consent URL. A customer-owned verified-publisher Azure app can improve branding/control and may reduce consent friction in tenants that allow user consent for verified publishers and the requested delegated permissions. It does not guarantee that no admin approval is needed: each Microsoft tenant's user-consent policy and the exact scopes requested still decide whether admin consent is required. ## Use Outlook through MCP and direct execution [#use-outlook-through-mcp-and-direct-execution] **Expect Tool Router meta-tools on Connect MCP.** `connect.composio.dev/mcp` uses Tool Router architecture, so it intentionally exposes meta-tools such as `COMPOSIO_SEARCH_TOOLS` and `COMPOSIO_MULTI_EXECUTE_TOOL`. The agent discovers and executes Outlook tools at runtime through those meta-tools. If you need specific Outlook tools without meta-tool round trips, use SDK direct execution or create a focused MCP config with selected Outlook tools. **Remove obsolete slugs from MCP configs.** If an Outlook MCP config fails due to obsolete or invalid tool slugs, update the MCP config to remove them and include only current supported tools in `allowed_tools`. This can be done through the dashboard or the MCP patch endpoint. **Pass attachment file paths through the SDK.** When using SDK automatic file handling for email attachments, pass the local file path directly in the `attachment`/`attachments` argument. Do not pass only a filename or raw content fields unless the tool schema explicitly asks for them. ## Target shared mailboxes and multiple accounts [#target-shared-mailboxes-and-multiple-accounts] **Pass the shared mailbox address as `user_id` or the mailbox target.** Delegated access must already be granted in the Microsoft tenant. This applies to delegated and S2S/application auth patterns where the tenant permissions allow shared mailbox access. **Select an aliased account on every multi-account call.** For multi-account Outlook sessions, every connected account needs a unique non-null alias, the session should set `multi_account.enable=true` and `require_explicit_selection=true`, and the LLM must set the `account` field on each item in `COMPOSIO_MULTI_EXECUTE_TOOL.tools[]`. Without explicit selection, Tool Router cannot disambiguate and may default to one account. --- # Perplexity AI authentication (/kb/guide/toolkits-perplexityai) ## Perplexity AI uses the `generic_api_key` connection field [#perplexity-ai-uses-the-generic_api_key-connection-field] The current `perplexityai` toolkit uses API-key authentication. Create the key in Perplexity's console and provide it as `generic_api_key` during connection initiation. The key is shown once by the provider, so store it in the customer's secret manager and never send it to support. If a first-party Perplexity tool succeeds but an equivalent Proxy Execute call returns 401 with the same connection, collect both Log IDs and the redacted request path. That comparison distinguishes a proxy auth-injection problem from an invalid provider key without asking the customer to rotate a working key. --- # Pipedrive (/kb/guide/toolkits-pipedrive) Use this guide to configure Pipedrive authentication, initiate connections, and use Pipedrive triggers. ## Configure Pipedrive authentication [#configure-pipedrive-authentication] **Use custom OAuth or API-key credentials.** Composio-managed OAuth is not currently available for Pipedrive. Create a custom auth config with your Pipedrive OAuth app, or use API-key authentication when that better fits your security requirements. **Pass the workspace subdomain during OAuth initiation.** When initiating a Pipedrive OAuth connection, pass the Pipedrive workspace subdomain or domain expected by the auth config. For example, if the workspace is `your-workspace.pipedrive.com`, pass `your-workspace` rather than the full hostname. **Complete custom OAuth setup through Composio.** Enable the app in Composio and complete setup there with your developer app credentials. Do not try to install the custom app directly from Pipedrive's OAuth app settings. During the Composio connection flow, provide the Pipedrive subdomain when requested. **Let hosted auth links collect required fields.** Use hosted auth links when you want Composio to collect required provider-specific fields during connection initiation. You can also inspect the auth config or toolkit metadata to see the expected input fields before starting the connection. ## Initiate Pipedrive connections and use triggers [#initiate-pipedrive-connections-and-use-triggers] **Pass a callback URL when initiating auth.** When initiating a Pipedrive connection through SDK or API, pass `callback_url` or `callbackUrl` in the connection initiation call. Composio redirects the user to that URL after the provider authentication flow completes. **Check the current trigger catalog before relying on a count.** Pipedrive has trigger support. Verify the current trigger list in the toolkit catalog before naming an exact count. --- # PostHog (/kb/guide/toolkits-posthog) ## PostHog is API-key based; use the PostHog API key when creating the connection [#posthog-is-api-key-based-use-the-posthog-api-key-when-creating-the-connection] PostHog is API-key based in Composio. Use the customer's PostHog API key when creating the connection. For connected-account creation, pass the key in the API-key auth state, for example with `generic_api_key` or the required field name returned by toolkit metadata. ## Configure PostHog subdomain for EU or self-hosted instances [#configure-posthog-subdomain-for-eu-or-self-hosted-instances] For EU or self-hosted PostHog instances, configure the PostHog `subdomain` or instance value instead of assuming the default cloud host. Inspect the current auth-config and connection-initiation fields to confirm where the active toolkit accepts that value. ## Pass auth config into Tool Router sessions; platform-created auth configs are not automatically usable [#pass-auth-config-into-tool-router-sessions-platform-created-auth-configs-are-not-automatically-usable] When using PostHog through Tool Router MCP, include the auth config in the Tool Router session so the generated MCP URL has the correct auth config details. Auth configs or connected accounts created on the platform side are not automatically available inside every Tool Router session unless they are passed/associated correctly. ## Create a PostHog integration/auth config before expecting it in auth\_configs API results [#create-a-posthog-integrationauth-config-before-expecting-it-in-auth_configs-api-results] `/api/v3/auth_configs` lists the active auth configs/integrations already created in the project. If PostHog is missing or the response is empty, create a PostHog auth config/integration first, then connect the account to it. ## Fetch PostHog tool schema to see required fields for a tool call [#fetch-posthog-tool-schema-to-see-required-fields-for-a-tool-call] If a PostHog tool call fails because of missing or mixed-up parameters, fetch the tool schema by slug, for example `/api/v3/tools/POSTHOG_CREATE_PROJECT_INSIGHTS_WITH_FORMAT_OPTION`, using the project API key. The schema response shows the required fields and expected shapes for that tool call. --- # QuickBooks (/kb/guide/toolkits-quickbooks) Use this guide to configure QuickBooks OAuth for the correct environment, maintain connections, and target the intended company account. ## Configure QuickBooks OAuth for the environment [#configure-quickbooks-oauth-for-the-environment] **Use the sandbox API base URL for sandbox accounts.** For QuickBooks sandbox accounts, pass `https://sandbox-quickbooks.api.intuit.com` as the URL/base URL when initiating the connection. Production connections should use the production Intuit API base URL. **Match Intuit credentials and the Composio redirect URL.** When creating a QuickBooks auth config, enter the QuickBooks OAuth credentials from the Intuit developer app and configure the Composio redirect URL in the QuickBooks auth app. A mismatch or missing redirect URL can break the OAuth flow. **Use current toolkit support for custom auth and token URLs.** QuickBooks toolkit support accepts auth and token URLs during connection initiation. If you need sandbox or custom Intuit OAuth endpoints, use a toolkit version that supports passing those URLs. **Request the payment scope only when payment access is enabled.** If the QuickBooks OAuth flow includes the payments scope `com.intuit.quickbooks.payment`, the QuickBooks payment module must be enabled for that account/app. If the customer does not need payment tools, remove that scope and retry the connection. ## Maintain the QuickBooks connection and auth experience [#maintain-the-quickbooks-connection-and-auth-experience] **Let Composio refresh tokens and retry transient failures.** QuickBooks OAuth refresh is handled by Composio through the provider's token endpoint. The current refresh path retries transient failures and uses credential-expiry timing rather than promising a fixed 15-minute schedule. If the provider conclusively rejects the grant or failures persist past the platform's retry budget, the connected account expires and the user must reauthenticate through a new auth link. **Send users directly to Intuit when the hosted auth screen should be skipped.** The Composio auth screen can be skipped for QuickBooks by sending users directly to the OAuth provider, following Composio's white-labeling/direct-provider auth flow. Use this when the customer wants the user to see the provider consent screen without the intermediate Composio auth screen. ## Target the correct QuickBooks account and toolkit version [#target-the-correct-quickbooks-account-and-toolkit-version] **Retry realm or company mapping issues on the latest toolkit version.** For QuickBooks realm/company mapping issues, retry on the latest toolkit version rather than a historical pinned version. **Use distinct account identifiers for multiple QuickBooks accounts.** Create separate connected accounts for each QuickBooks account, preferably with distinct `user_id` values. In Claude/MCP setup, append the desired `connected_account_id` or `user_id` to the MCP URL/configuration so the session targets the intended QuickBooks connection. --- # Reddit (/kb/guide/toolkits-reddit) ## Use Connect MCP for Reddit OAuth callback failures in Claude Code [#use-connect-mcp-for-reddit-oauth-callback-failures-in-claude-code] For Claude Code Reddit MCP OAuth callback failures on the legacy MCP path, switch the MCP server URL to `https://connect.composio.dev/mcp`. Remove the old `x-api-key` header and configure the current `x-consumer-api-key` header from the AI Clients setup. Connect MCP can then start the Reddit authorization flow from the client. ## Reddit supports managed and customer-owned OAuth 2.0 [#reddit-supports-managed-and-customer-owned-oauth-20] Use Composio-managed OAuth for the standard connection flow. Create a custom auth config with the customer's Reddit client ID and client secret when they need control over provider app settings and credentials. Make sure Reddit has approved a custom app for its intended access before using it in production. ## Reddit toolkit behavior can change when Reddit changes its API or enforcement policies [#reddit-toolkit-behavior-can-change-when-reddit-changes-its-api-or-enforcement-policies] The Reddit toolkit depends on Reddit's underlying APIs and policy enforcement. Changes or restrictions from Reddit can affect toolkit behavior, and Reddit does not guarantee stable API behavior for all use cases. For production usage, use your own Reddit credentials to maximize control, and account for Reddit's spam and responsible builder policies when designing automations. ## Older Reddit Create Post tool versions may require `flair_id` [#older-reddit-create-post-tool-versions-may-require-flair_id] If Reddit Create Post fails on version `00000000_00`, check whether the request is missing `flair_id`; that old version requires it. Prefer pinning a specific current toolkit/tool version to avoid breaking changes. In recent Reddit tool versions, `flair_id` is no longer required for the Create Post call. --- # Salesforce (/kb/guide/toolkits-salesforce) Use this guide to configure Salesforce OAuth and domains, troubleshoot connected-app access, choose current tools, and build UI bridge flows safely. ## Configure Salesforce OAuth and connection flows [#configure-salesforce-oauth-and-connection-flows] **Use customer-owned credentials for app-level control.** The current Salesforce toolkit supports OAuth2 and server-to-server OAuth2 with customer-owned credentials. Configure the Salesforce connected app according to Salesforce's OAuth guidance and use its credentials in a custom Composio auth config. This gives the customer control over scopes, branding, and provider-side policy. **Choose hosted auth or direct initiation based on who supplies required fields.** The Salesforce field collection interface is part of Hosted Authentication / the connection link flow. If you want Composio to collect required fields, use hosted auth. If your app already knows the Salesforce instance/subdomain values, skip that interface and call `.initiate()` directly with the required fields. Use `.refresh()` to regenerate the auth URL for an already initiated connection; `.link()` starts a new connection. If you truly need multiple connections for the same `user_id`, pass `allow_multiple=True` to `.initiate()`. **Match the redirect URI to the current Composio callback.** Use the callback URL shown by the current Composio auth-config flow as the authorized redirect URI for custom Salesforce OAuth. This provider callback is separate from the post-auth customer redirect passed as `callback_url` / `callbackUrl` during connection initiation. ## Set the Salesforce domain and connection fields [#set-the-salesforce-domain-and-connection-fields] **Provide the instance endpoint and My Domain subdomain.** Salesforce accepts additional connection initiation fields. Fetch the toolkit by slug (`/api/v3.1/toolkits/salesforce`) to inspect the expected fields, and fetch the connected account to see the same fields after connection. The important Salesforce fields are `My Domain Subdomain` and `Instance endpoint`. If you are initiating directly through the SDK/API, pass these fields through `.initiate()` rather than waiting for the hosted connection UI. **Use the My Domain or API prefix when `login` is not enough.** For Salesforce, the default subdomain value is `login`, and that works in most cases. If the default or a simple org label fails, Composio needs the Salesforce login/API domain prefix rather than the full browser URL. Use these formats: * Default case: keep `login`. * Standard My Domain URL: for `https://your-company.my.salesforce.com/...`, pass `your-company.my`. * Developer Edition / Lightning URL: for `https://.develop.lightning.force.com/...`, the matching OAuth/My Domain host is usually `https://.develop.my.salesforce.com/...`, so pass `.develop.my`. If the customer enters only ``, Composio may generate `.salesforce.com`, which can fail before OAuth with a browser DNS error such as `DNS_PROBE_FINISHED_NXDOMAIN`. **Recheck the domain when Salesforce returns `URL_NOT_RESET`.** `URL_NOT_RESET` can happen when the Salesforce org requires a specific My Domain value but the connection is using the generic `login` default or an incomplete subdomain. The default `login` value is fine for most Salesforce flows, but for org-specific failures recheck the Salesforce domain/subdomain values on the connection, pass the correct My Domain subdomain, and retry on the latest toolkit version if the issue was seen on an older pinned version. ## Troubleshoot connected-app access and token policies [#troubleshoot-connected-app-access-and-token-policies] **Ask an org admin to install or approve restricted connected apps.** Salesforce connected app usage restrictions can require an org admin to install or approve the connected app before org users can authenticate. Check whether the error URL includes `error=invalid_client&error_description=app+must+be+installed+into+org`. In Salesforce Setup, go to OAuth Connected App Usage and look for the app with an Install button in the Actions column. After the admin installs/enables the app, users should retry authentication. **Account for Salesforce's five active refresh-token limit.** Salesforce allows only five active refresh tokens per user per connected app. When the same Salesforce user connects a sixth time, Salesforce can revoke the oldest refresh token, which makes older Composio connected accounts fail with token errors. Also check whether the user changed their password, revoked the app, changed connected app refresh-token policy away from `valid until revoked`, or has org-level session policies that invalidate tokens. ## Discover and use current Salesforce tools [#discover-and-use-current-salesforce-tools] **Inspect object schemas before querying or updating them.** Use `SALESFORCE_GET_ALL_FIELDS_FOR_OBJECT` when you need to inspect the fields available on a Salesforce object. This is the right tool for schema discovery before building object-specific queries or update flows. **Replace deprecated retrieve actions with current get and list tools.** Use the current Salesforce tool slugs instead of the deprecated retrieve variants: `SALESFORCE_RETRIEVE_LEAD_BY_ID` -> `SALESFORCE_GET_LEAD`, `SALESFORCE_RETRIEVE_SPECIFIC_CONTACT_BY_ID` -> `SALESFORCE_GET_CONTACT_BY_ID`, and `SALESFORCE_RETRIEVE_OPPORTUNITIES_DATA` -> `SALESFORCE_LIST_OPPORTUNITIES`. **List contacts before fetching a specific contact by ID.** Use `SALESFORCE_LIST_CONTACTS` to list contacts and capture the IDs with their names. Then call `SALESFORCE_GET_CONTACT_BY_ID` with the desired contact ID to fetch the specific contact details. ## Use Proxy Execute for Salesforce UI bridge flows [#use-proxy-execute-for-salesforce-ui-bridge-flows] Do not build Salesforce Frontdoor/UI bridge flows by reading access tokens from the connected account API. Use Proxy Execute with the Salesforce connected account instead. Composio injects the OAuth access token server-side into the proxied Salesforce request, such as a call to `/services/oauth2/singleaccess`, and Salesforce returns the frontdoor URI that the application can redirect the user's browser to. --- # SerpApi (/kb/guide/toolkits-serpapi) ## Disable SerpAPI by listing premium toolkit slugs in session config [#disable-serpapi-by-listing-premium-toolkit-slugs-in-session-config] There is no single global toggle for premium tools. To prevent SerpAPI from being available in a session, list `serpapi` in the disabled toolkit slugs for the session config. Other premium slugs commonly disabled together include `composio_search`, `perplexityai`, `exa`, and `codeinterpreter`. ## Use toolkit details to inspect SerpAPI required auth fields [#use-toolkit-details-to-inspect-serpapi-required-auth-fields] Use `.toolkits.get("serpapi")` to fetch the toolkit details, including required and optional auth fields. For SerpAPI, the connection initiation payload should include a required `generic_api_key` field displayed as `API Key`. ## Search and scraping use cases can use SerpAPI alongside Firecrawl, Exa, Tavily, or Composio Search [#search-and-scraping-use-cases-can-use-serpapi-alongside-firecrawl-exa-tavily-or-composio-search] For search and scraping use cases, Composio has multiple relevant toolkits: SerpAPI, Firecrawl, Exa, Tavily, and Composio Search. Composio Search provides search providers such as Exa and Tavily without separate auth. --- # ServiceNow authentication (/kb/guide/toolkits-servicenow) ## ServiceNow credentials and instance subdomain are collected at different stages [#servicenow-credentials-and-instance-subdomain-are-collected-at-different-stages] The current `servicenow` toolkit supports Basic, OAuth2, and S2S OAuth2. OAuth auth-config creation accepts the ServiceNow application's client ID and client secret. Connection initiation then requires the instance subdomain, such as `mycompany` for `mycompany.service-now.com`. ServiceNow registers an inbound OAuth client inside a ServiceNow instance. A client registered in one customer instance does not automatically authorize an unrelated instance. Use a separate customer-owned auth config when different customers supply different ServiceNow application registrations. Use delegated OAuth2 when a user should sign in and consent. Use S2S OAuth2 for a backend integration whose identity and permissions are configured by the ServiceNow administrator. Fetch the current toolkit metadata before building a form because the required fields are separated into `auth_config_creation` and `connected_account_initiation`. --- # SharePoint (/kb/guide/toolkits-sharepoint) ## SharePoint REST APIs and SharePoint Graph are separate API families [#sharepoint-rest-apis-and-sharepoint-graph-are-separate-api-families] Treat the current SharePoint toolkit and the SharePoint Graph toolkit as separate API families, not as interchangeable variants of the same connection. * The current SharePoint toolkit uses SharePoint REST/OData endpoints on the tenant SharePoint host, usually shaped like `https://.sharepoint.com/_api/...`. * The SharePoint Graph toolkit uses Microsoft Graph endpoints, usually shaped like `https://graph.microsoft.com/v1.0/sites/...`. * The scopes and token audience must match the endpoint family. SharePoint REST expects a SharePoint resource token such as `https://.sharepoint.com/.default`; SharePoint Graph expects Microsoft Graph permissions/scopes such as `Sites.*`, `Files.*`, `User.Read`, or `https://graph.microsoft.com/.default` for S2S. * Do not add Microsoft Graph scopes such as `Sites.Read.All` or `User.Read.All` to the current SharePoint REST auth config as a workaround. Those scopes produce a Graph-audience token and can cause 401 responses when the toolkit calls SharePoint REST. * Do not reuse an existing SharePoint REST connected account/token for SharePoint Graph. A Graph-scoped token is not valid for SharePoint REST, and a SharePoint-audience token is not valid for Graph. * The same Microsoft Entra app registration may be reused only if it has the right API permissions and redirect/client-credential setup for the target toolkit, but create or use a separate Composio auth config and reconnect. SharePoint REST is not deprecated just because SharePoint Add-Ins / Azure ACS are retiring. Microsoft still documents SharePoint REST/CSOM as valid when Graph does not cover the needed functionality. Graph is the unified Microsoft 365 API and is usually better for cross-service or client-secret S2S flows, but it does not have perfect parity with SharePoint REST. Example response: ```text The SharePoint and SharePoint Graph toolkits use different Microsoft API surfaces. The existing SharePoint toolkit uses SharePoint REST/OData endpoints such as `https://.sharepoint.com/_api/...` and needs a SharePoint-resource scope like `https://.sharepoint.com/.default`. The SharePoint Graph toolkit uses Microsoft Graph endpoints such as `https://graph.microsoft.com/v1.0/sites/...` and needs Graph permissions such as `Sites.*`, `Files.*`, `User.Read`, or for S2S `https://graph.microsoft.com/.default`. Because the tokens are issued for different resources, please create/use a separate Composio auth config for SharePoint Graph and reconnect. You may be able to reuse the same Microsoft Entra app registration if it has the required Graph permissions configured, but the existing SharePoint connected account token should not be used for SharePoint Graph. ``` ## `/teams/` SharePoint sites require the server-relative Subsite path [#teams-sharepoint-sites-require-the-server-relative-subsite-path] If your SharePoint site URL is under `/teams/` instead of `/sites/`, do not pass only `` in the SharePoint Subsite field. A bare subsite value is interpreted as `/sites/` by the toolkit. Reconnect the SharePoint account and set SharePoint Subsite to the full server-relative path, for example `/teams/`. For per-call overrides, pass `site_name: "/teams/"`. Debugging signal: tool logs show SharePoint calls like `https://tenant.sharepoint.com/sites//_api/...` returning `404 FILE NOT FOUND`, while the customer's actual SharePoint URL is `https://tenant.sharepoint.com/teams/`. If the connected account is `ACTIVE` and the auth config is enabled, treat this as a path-prefix mismatch first, not an OAuth issue. Example response: ```text This looks like a SharePoint site-path mismatch. Your site is under `/teams/...`, but the current connection/tool call is hitting `/sites/...`, which SharePoint returns as 404. Please reconnect the SharePoint account and set the Subsite value to the full server-relative path: `/teams/`. If you're passing it per tool call, use `site_name: "/teams/"`. A bare value like `` gets treated as `/sites/`. ``` ## SharePoint REST app-only client credentials use certificate auth [#sharepoint-rest-app-only-client-credentials-use-certificate-auth] For the current Composio `share_point` toolkit, client credentials and certificate-based authentication are the same app-only path: client credentials is implemented with certificate-based authentication. The required setup is: * SharePoint tenant name, used for `https://.sharepoint.com/_api` and the resource scope `https://.sharepoint.com/.default` * Microsoft Entra tenant ID * Application/client ID * RSA private key in PEM format for the certificate uploaded to the Entra app registration * Certificate thumbprint (`x5t#S256`) * Admin-consented SharePoint application permissions appropriate for the use case Composio signs a JWT client assertion with the certificate/private key and requests a token from `https://login.microsoftonline.com//oauth2/v2.0/token` using `grant_type=client_credentials` and the SharePoint `.default` scope. Customers should provide the fields above; they do not need to manually construct or pass a JWT assertion. Scope wording: the token request uses `https://.sharepoint.com/.default`. In Microsoft client credentials, `.default` means the token is issued for the application permissions/app roles already configured and admin-consented for that SharePoint resource. Composio's action-to-scope mapping API should not be recommended for this SharePoint S2S/certificate path today; it is useful for OAuth2 scope discovery, not as the S2S permission source of truth. Do not offer a client-secret-only client-credentials setup for the SharePoint REST toolkit. That belongs to Microsoft Graph app-only flows and Composio's `sharepoint_graph` toolkit, which uses `https://graph.microsoft.com/.default` and accepts client ID + client secret. The legacy SharePoint Azure ACS app-only client ID/secret model existed but is retired and should not be recommended for new/current SharePoint REST integrations. Example setup outline: 1. Generate a private key and self-signed/public certificate, for example with OpenSSL. 2. Upload the public certificate to the Microsoft Entra app registration under Certificates & secrets > Certificates. 3. Add/admin-consent SharePoint application permissions, such as the least-privileged site/list/file permission set appropriate for the customer. 4. Create/connect the Composio SharePoint S2S auth config with the SharePoint tenant name, Entra tenant ID, client ID, private key PEM, and certificate thumbprint. Composio handles the JWT client assertion and token exchange. 5. Ensure the Entra app has/admin-consented the SharePoint application permissions needed for the intended SharePoint REST operations. The requested token scope is `https://.sharepoint.com/.default`; if using Selected permissions such as `Sites.Selected`, also grant explicit access to the target site/list/file. 6. Test with a simple SharePoint REST call such as `GET https://.sharepoint.com/_api/web?$select=Title` using the connected account. ## SharePoint `.default` scope uses the tenant domain placeholder [#sharepoint-default-scope-uses-the-tenant-domain-placeholder] For a custom Microsoft Entra app, replace `{{site_name}}` in `https://{{site_name}}.sharepoint.com/.default` with the customer's SharePoint tenant/domain name. The resulting `.default` scope requests the application permissions already configured and admin-consented for that SharePoint resource. ## Pass the SharePoint tenant/subdomain during connection initiation [#pass-the-sharepoint-tenantsubdomain-during-connection-initiation] The SharePoint tenant/subdomain is an explicit connection field; Composio does not derive it automatically from the OAuth token. If a connection points at `default.sharepoint.com` or the wrong tenant, reinitiate the connection and provide the correct tenant name. ## The SharePoint subsite field is not a permission boundary [#the-sharepoint-subsite-field-is-not-a-permission-boundary] The SharePoint Subsite field supplies a default target when a tool call omits `site_name`. It does not restrict the Microsoft token, which retains the access granted to the consenting user or application. ## Retrieve SharePoint site name from connected account state [#retrieve-sharepoint-site-name-from-connected-account-state] Fetch the connected account and inspect its stored state to confirm the SharePoint site name. Newer SDK responses expose it under a shape such as `state.val.site_name`; older toolset responses may expose `data.site_name`. ## Use `SHARE_POINT_SEARCH_QUERY` for KQL/FQL SharePoint search [#use-share_point_search_query-for-kqlfql-sharepoint-search] Use `SHARE_POINT_SEARCH_QUERY` when a workflow needs flexible SharePoint search with KQL or FQL. For broader agentic discovery across SharePoint actions, Tool Router can discover and execute the relevant tools dynamically. ## SharePoint toolkit slug is `share_point` [#sharepoint-toolkit-slug-is-share_point] The SharePoint toolkit slug is `share_point`, while its tool slugs use the `SHARE_POINT_...` prefix. Related Microsoft toolkit slugs include `outlook`, `one_drive`, and `sharepoint_graph`. ## Disable destructive SharePoint tools with `destructiveHint` or explicit tool filters [#disable-destructive-sharepoint-tools-with-destructivehint-or-explicit-tool-filters] At session creation, disable tools carrying `destructiveHint` globally or for selected toolkits such as SharePoint and OneDrive. For finer control, explicitly allow or deny destructive tools by name. ## `SHARE_POINT_UPLOAD_FROM_URL` needs a server-fetchable URL [#share_point_upload_from_url-needs-a-server-fetchable-url] This action first downloads `file_url` from Composio's backend and then uploads the bytes to SharePoint. The source must be a reachable HTTP(S) download URL; raw base64 content is not a URL. For base64 or in-memory bytes, use `SHARE_POINT_UPLOAD_FILE` with the file content and name, or first create a temporary URL that the backend can reach. A 401/403 while downloading the source should be debugged as source-URL access, not as a destination folder problem. `conflict_behavior="rename"` only affects the target name after download succeeds. --- # Shopify (/kb/guide/toolkits-shopify) Use this guide to configure Shopify authentication, discover the complete tool set, and work with Shopify orders and GraphQL. ## Configure Shopify authentication [#configure-shopify-authentication] **Use OAuth2 or S2S auth instead of API-key/admin-token auth.** Shopify deprecated the old admin-created custom-app token copy/paste path for new apps. New Dev Dashboard apps expose a Client ID and Client Secret, and the access token is generated programmatically with Shopify's client-credentials flow. In Composio, do not direct new Shopify users to API-key/Admin API Access Token auth. Use OAuth2 for user-facing Shopify integrations, or S2S auth when that matches the app's server-to-server/client-credentials use case. * Shopify docs: [client credentials grant](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/client-credentials-grant), [admin-created custom app tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/generate-app-access-tokens-admin) If an auth screen still asks for an Admin API key, verify the authConfig is not using the deprecated API-key mode. **Use Composio's toolkit auth callback as the OAuth redirect URL.** Set the Shopify OAuth app redirect URL to the exact callback shown by the current Composio custom-auth-config flow. Older or mistyped v1/v3 callback paths can cause OAuth redirect failures, so copy the current value rather than relying on a hard-coded URL in this article. **Keep the hosted auth experience when using custom credentials.** Using custom Shopify OAuth credentials does not change the end-user hosted auth and redirect experience. Users still go through the same Composio connect flow, and Composio continues to handle token refresh and credential management automatically. Masking changes for managed credentials do not affect custom-credential toolkits in the same way. **Check credentials and gated scopes when OAuth returns 400.** A Shopify OAuth 400 during token exchange or connection initiation is commonly caused by incorrect credentials, especially a wrong client secret, or by gated scopes that have not been verified/approved. Re-enter the authConfig client secret carefully and initiate a fresh connection. Also verify the requested Shopify scopes are available to the app. **Pass only the store name as the Shopify subdomain.** When Composio asks for the Shopify subdomain, pass only the store name, such as `your-store-name`. Do not pass the full host like `your-store-name.myshopify.com`; Composio constructs the Shopify domain from the subdomain. ## Discover and run Shopify tools [#discover-and-run-shopify-tools] **Use the current GraphQL tool slug.** Use the updated Shopify GraphQL tool slug `SHOPIFY_GRAPH_QL_QUERY` for Shopify GraphQL queries. If the tool is not visible in tool discovery, make sure enough tools are being fetched and that the tool is enabled in the MCP/config being used. **Fetch more than the default 20 tools when needed.** Tool fetching can default to a limited number of tools. Pass a higher `limit`, for example `tools.get(user_id="", toolkits=["shopify"], limit=1000)`, to fetch the full Shopify tool set. For MCP, also confirm the target Shopify tool is enabled when creating the MCP config or by modifying the existing config. **Create custom Shopify tools with Composio-injected auth.** Create a custom tool/action under the Shopify toolkit and call Shopify's GraphQL endpoint from inside it. Composio injects the Shopify auth automatically through the custom tool execution path. For newer examples, the endpoint can be `/graphql.json`; older snippets used the full `https://.myshopify.com/admin/api//graphql.json` endpoint. Include the JSON content type header and pass the GraphQL query in the body. ## Work with Shopify orders [#work-with-shopify-orders] **List orders before running follow-up actions.** Call `SHOPIFY_GET_ORDERS_WITH_FILTERS` first to confirm the store has orders and retrieve order IDs from the response payload. Follow its `page_info` cursor when more than one page may match. Then pass a returned order ID into follow-up actions such as `SHOPIFY_GET_ORDER` or `SHOPIFY_UPDATE_ORDER`. The older `SHOPIFY_GET_ORDER_LIST` action is deprecated. **Check `read_all_orders` when order calls return 403.** Check the scopes on the Shopify connection. If the connection lacks `read_all_orders`, reconnect with the needed order scopes before retrying order update/read calls that require access beyond the default order scope set. --- # Slack (/kb/guide/toolkits-slack) ## Use `user_scopes` for Slack user-token permissions [#use-user_scopes-for-slack-user-token-permissions] For the Slack toolkit, `scopes` refers to bot-user scopes. If the use case is to operate as the actual Slack user, pass the permissions in `user_scopes` on the auth config credentials. Slack is special because it separates bot scopes from user scopes. For user-token tools, set `credentials.user_scopes`; the bot `scopes` field may not matter if the Slack application has no bot-user tools for that use case. ## Download Slack file content using file ID [#download-slack-file-content-using-file-id] Slack file download is supported through `SLACK_DOWNLOAD_SLACK_FILE`. Pass the Slack file ID, which starts with `F` such as `F123ABCDEF0`. The tool returns downloadable file content plus metadata such as name, mimetype, and size. If the file ID is unknown, first call `SLACK_LIST_FILES_WITH_FILTERS_IN_SLACK` to find file IDs, then pass the selected ID to the download tool. ## Slack `assistant.search.context` requires Agents & AI Apps and Business+ [#slack-assistantsearchcontext-requires-agents--ai-apps-and-business] Slack's `assistant.search.context` requires the Slack OAuth app to have the Agents & AI Apps feature enabled, and the Slack workspace must be on Business+ or higher. Verify workspace support by calling `assistant.search.info`; if `is_ai_search_enabled` is `false`, the workspace plan or feature enablement is the blocker. A customer can unblock with their own Slack OAuth app that has Agents & AI Apps enabled, but they still need Business+ on the workspace. ## Use Slack V2 trigger slugs for channel and direct messages [#use-slack-v2-trigger-slugs-for-channel-and-direct-messages] Use the Slack V2 triggers for message events. `SLACK_CHANNEL_MESSAGE_RECEIVED` is intended for channel messages, and `SLACK_DIRECT_MESSAGE_RECEIVED` is intended for DMs. Slack V2 triggers include dedicated endpoints, signature verification, better DM handling, and richer filtering. Older V1 Slack trigger slugs may still work, but V2 is the recommended path for new setups. ## Slack trigger delivery depends on the Slack app event subscription webhook URL [#slack-trigger-delivery-depends-on-the-slack-app-event-subscription-webhook-url] When Slack trigger events stop unexpectedly, check whether the Slack OAuth app's Event Subscriptions `webhook_url` was changed. If the webhook URL or other Slack app event-subscription settings changed, Slack may stop delivering events to Composio even though the trigger instance was previously working. ## Slack short connect links are not the OAuth redirect URI [#slack-short-connect-links-are-not-the-oauth-redirect-uri] The short `/api/v3/s/...` URL is not the `redirect_uri` sent to Slack. It is only a shortened link that redirects the browser to Slack's authorization page. The actual Redirect URI is available in the authConfig and must match what is configured in the Slack OAuth app. The static `callbackUrl` / `redirectUri` must be configured consistently on both Composio and the Slack OAuth app, while `redirectUrl` is the per-connection authentication URL used to send the user through the auth flow. ## Slack scheduled-message attachments are not file uploads [#slack-scheduled-message-attachments-are-not-file-uploads] The `attachments` field on Slack scheduled messages refers to Slack's legacy secondary/rich-formatting attachments, not uploaded files. Slack's `chat.scheduleMessage` API does not natively upload files. Files must be uploaded separately, for example with `files.upload` / `files.upload.v2`, and then linked or embedded into the scheduled message body so they unfurl when the scheduled message is posted. ## `admin.conversations:write` requires Slack Enterprise [#adminconversationswrite-requires-slack-enterprise] `admin.conversations:write` is an enterprise/admin-level Slack scope. For APIs such as `admin.conversations.delete`, the Slack workspace must be on an Enterprise plan. If you cannot use channel deletion/admin conversation tools, first confirm the Slack workspace plan and whether the app has the required admin scope. --- # Slackbot (/kb/guide/toolkits-slackbot) Use this guide to choose the correct Slack token model, configure Slackbot scopes and triggers, and send or download Slack content. ## Choose Slack or Slackbot and configure authentication [#choose-slack-or-slackbot-and-configure-authentication] **Match the toolkit to the token model.** Slack and Slackbot serve different token models. The Slack toolkit performs actions on behalf of an actual Slack user. The Slackbot toolkit performs actions as a bot and should be used for bot scopes such as `channels:join` or bot-token workflows. For mixed use cases, create separate Slack and Slackbot auth configs rather than combining user and bot scopes in one connection. **Include the verification token for custom Slackbot triggers.** For Slackbot triggers with custom auth, configure the Slack app verification token in the auth config, then create a fresh connection after updating the auth config. The current auth schema does not expose a separate subscription-ID field, so do not substitute one for the verification token. **Add history scopes for private channels and DMs.** Slack private-channel and DM access requires additional scopes. Use `groups:history` for private channels, `im:history` for direct messages, and `mpim:history` for multi-person DMs. These scopes are not always included by default and may be limited by Slack plan/provider constraints, so the customer may need a custom Slack app with the relevant scopes. **Do not use a short auth link as the OAuth redirect URI.** The short `/api/v3/s/...` auth link is only a shortened connection initiation URL that redirects the browser to Slack. It is not the `redirect_uri` sent to Slack. Configure the static redirect/callback URI shown in the Composio auth config in the Slack OAuth app; either supported v1 or v3 callback URI can be used depending on the auth config. ## Run Slackbot actions and handle trigger events [#run-slackbot-actions-and-handle-trigger-events] **Resolve the Slack file ID before downloading content.** Slack file content can be downloaded with `SLACK_DOWNLOAD_SLACK_FILE`. The tool needs the Slack file ID, usually starting with `F`. If the customer does not have the file ID yet, use `SLACK_LIST_FILES_WITH_FILTERS_IN_SLACK` first and pass the returned file ID to the download tool. **Choose one visible content mode when sending a bot message.** Use `SLACKBOT_SEND_MESSAGE` to post to a channel, direct message, or private group. Provide exactly one visible content mode: `markdown_text` for normal Markdown content, or `blocks` for a raw Block Kit layout. Use `fallback_text` only with `blocks`. **Use trigger identifiers to map events back to connections.** Slackbot trigger payloads include identifiers such as `connection_id` and `trigger_id` inside the payload data. Use `connection_id` to map the event back to the connected account involved in the trigger. --- # Snapchat authentication (/kb/guide/toolkits-snapchat) ## Snapchat uses customer-owned OAuth credentials [#snapchat-uses-customer-owned-oauth-credentials] The current `snapchat` toolkit supports OAuth2 and requires a Snapchat app's client ID and client secret. Register the exact redirect URI shown by the current Composio auth-config flow and request only the Snapchat permissions approved for that app. If Snapchat rejects the authorization request before the user signs in, verify the client ID, redirect URI, and approved scopes on the Snapchat app. A pre-login authorization error is not evidence that the user's Snapchat password is wrong. --- # Snowflake (/kb/guide/toolkits-snowflake) ## Use one Snowflake auth config per customer account for multi-tenant SaaS OAuth [#use-one-snowflake-auth-config-per-customer-account-for-multi-tenant-saas-oauth] For Snowflake multi-tenant OAuth, create one Composio auth config per customer Snowflake account using that customer's Snowflake OAuth credentials from `CREATE SECURITY INTEGRATION`. Store the returned `auth_config_id` against the customer on your side. When connecting a user, pass the correct `auth_config_id`; Composio will collect the per-connection Account ID, such as `myorg-myaccount`, and use it to construct the Snowflake authorization/token URLs. ## Snowflake Basic auth was deprecated in favor of OAuth2 [#snowflake-basic-auth-was-deprecated-in-favor-of-oauth2] Snowflake Basic authentication was deprecated and replaced by OAuth2. Customers using old Basic-auth Snowflake auth configs or connected accounts should migrate to OAuth2 by creating the Snowflake OAuth security integration, creating a Composio auth config with those credentials, and reconnecting users. Basic-auth actions may differ from OAuth2 actions and should not be treated as the long-term path. ## Configure Snowflake OAuth refresh tokens and expect periodic reconnects [#configure-snowflake-oauth-refresh-tokens-and-expect-periodic-reconnects] For longer-lived Snowflake OAuth connections, configure the Snowflake security integration with `OAUTH_ISSUE_REFRESH_TOKENS = TRUE` so refresh tokens are issued, and set `OAUTH_REFRESH_TOKEN_VALIDITY` as high as Snowflake allows, such as 7776000 seconds (about 90 days). Even with the max window, Snowflake can require users to reconnect after the refresh-token validity period, so design the product flow to handle periodic reconnects. ## Fetch connected-account fields or toolkit metadata to discover Snowflake account details [#fetch-connected-account-fields-or-toolkit-metadata-to-discover-snowflake-account-details] To discover fields collected during connection initiation, call the toolkit-by-slug endpoint and inspect the accepted initiation fields. After a connection exists, fetch the connected account by ID to retrieve the stored connection fields. Provider schemas are mostly static, but providers can change them, so the toolkit metadata endpoint is the safer source for current required/accepted fields. ## Snowflake statement results may require checking each partition [#snowflake-statement-results-may-require-checking-each-partition] If a Snowflake query returns partial results, check whether the result set is split into partitions. Snowflake may not return all partitions in a single tool call. Use `SNOWFLAKE_CHECK_STATEMENT_STATUS` with the statement handle to poll an asynchronous query until it finishes and retrieve its result. ## Use processors or tool description overrides to reduce Snowflake tool output/token load [#use-processors-or-tool-description-overrides-to-reduce-snowflake-tool-outputtoken-load] For Snowflake tools that return too much data or need LLM-facing schema or description changes, use processors to post-process tool output before returning it to the model. For a local agent setup, you can also modify a returned tool object's description before passing it to the model. --- # Spotify (/kb/guide/toolkits-spotify) Use this guide to configure Spotify OAuth and scopes, then use Spotify through MCP, custom toolkits, and triggers. ## Configure Spotify OAuth and scopes [#configure-spotify-oauth-and-scopes] **Use a customer-owned OAuth app.** Composio-managed OAuth is not currently available for Spotify. Create a custom auth config with your Spotify client ID and client secret, then have each user complete the Spotify authorization flow. **Add library scopes before reconnecting.** If Spotify tools need library access, ensure scopes such as `user-library-read` and `user-library-modify` are present in the auth config. After adding scopes, reconnect so the connected account receives the new grants. **Add playlist-write scopes before reconnecting.** If playlist write actions return Spotify `403 Insufficient client scope`, make sure the auth config requests `playlist-modify-public`, `playlist-modify-private`, or both as appropriate. Add the scopes before reconnecting; reconnecting an unchanged auth config preserves the same missing-scope problem. This is separate from older playlist endpoint issues. A call can reach the current `/items` endpoint and still fail because its token lacks playlist write permission. ## Use Spotify through MCP, custom toolkits, and triggers [#use-spotify-through-mcp-custom-toolkits-and-triggers] **Avoid names that collide with the built-in toolkit.** If creating a custom Spotify-related toolkit, avoid naming it exactly `Spotify` because a built-in Spotify toolkit already exists. Use a distinct name such as `spotify-custom` to avoid slug or name collision errors. **Add Spotify from the MCP configs page.** To use Spotify through MCP, create or edit an MCP config from the platform MCP configs page and add Spotify to that server. Then use the generated MCP URL in the MCP client. **Check the current trigger catalog when an event is missing.** Spotify is listed among trigger-capable toolkits, with three Spotify triggers. If the event you need is missing, submit it through the standard trigger-request flow. --- # Strava (/kb/guide/toolkits-strava) ## Athlete-limit errors belong to the OAuth application [#athlete-limit-errors-belong-to-the-oauth-application] Strava applies connected-athlete capacity per developer application. If OAuth shows `Athlete limit exceeded`, first determine whether the auth config uses Composio-managed Strava credentials or a customer-owned app; do not assume the customer owns a managed app. For dedicated production capacity, create a customer-owned Strava developer app, configure it as a custom Composio auth config, and request any capacity increase from Strava for that app. The exact current capacity is visible to the app owner in Strava's API settings and can change, so do not quote a customer-specific number without checking it. See the [Strava custom OAuth setup guide](https://composio.dev/auth/strava) for credential setup. --- # Stripe (/kb/guide/toolkits-stripe) ## Stripe is supported and offers OAuth2 and API-key auth modes [#stripe-is-supported-and-offers-oauth2-and-api-key-auth-modes] Composio supports the Stripe toolkit with OAuth2 and API-key auth modes; the marketplace entry is available on the Stripe toolkit page. ## For Stripe API-key auth, use the Stripe secret key from Developers -> API Keys -> Standard keys [#for-stripe-api-key-auth-use-the-stripe-secret-key-from-developers---api-keys---standard-keys] For Stripe API-key auth, use the Stripe secret key from Stripe Dashboard -> Developers -> API Keys -> Standard keys -> Secret key. In API/SDK connection payloads, the auth config field may need to be passed as `api_key`. ## One Stripe MCP/API-key connection maps to one Stripe account unless the customer uses Stripe Connect [#one-stripe-mcpapi-key-connection-maps-to-one-stripe-account-unless-the-customer-uses-stripe-connect] Stripe usually uses different API keys for separate accounts, so one connected account/MCP server has access to one Stripe account. If the customer uses Stripe Connect, the platform can consolidate connected accounts under one platform API key and may better fit multi-account workflows. ## MRR can be calculated from `STRIPE_LIST_SUBSCRIPTIONS` [#mrr-can-be-calculated-from-stripe_list_subscriptions] Use `STRIPE_LIST_SUBSCRIPTIONS` to retrieve subscription data, then calculate MRR from the returned subscriptions in the agent/application layer. ## Stripe payment-success triggers are available [#stripe-payment-success-triggers-are-available] Use `STRIPE_INVOICE_PAYMENT_SUCCEEDED_TRIGGER` for successful invoice payments and `STRIPE_CHECKOUT_SESSION_COMPLETED_TRIGGER` for completed Checkout sessions. Fetch the current trigger catalog before implementation rather than assuming every Stripe event has a corresponding trigger. --- # Supabase (/kb/guide/toolkits-supabase) Use this guide to connect Supabase, configure its tools and endpoints, and troubleshoot permissions or rate limits. ## Connect Supabase with OAuth or an API key [#connect-supabase-with-oauth-or-an-api-key] **Confirm the authorized Supabase organization.** Supabase authorization is usually scoped at the organization level. If you have project or account access issues, confirm which Supabase organization/account the connected credentials belong to before treating it as a tool-specific issue. **Pass the personal token with the required API-key field.** For Supabase API-key auth, create or use an API-key auth config and pass the personal token as `supabase_personal_token` when creating the connected account. The `/api/v3/toolkits/supabase` endpoint can be used to inspect the required connected-account initiation field name. **Choose either OAuth2 or API\_KEY auth.** Supabase supports OAuth2 and API\_KEY auth, and both can be initiated through Composio APIs. SDKs are wrappers over the same APIs, so anything possible through the SDK should be possible through the API. **Initiate the connection explicitly in Cursor.** Ask Cursor/the MCP client to initiate a Supabase connection first. The MCP server should provide an OAuth link, the user completes authentication, and then Supabase tools can execute against the connected account. ## Configure Supabase tools and endpoints [#configure-supabase-tools-and-endpoints] **Add the SQL tool to the MCP server when needed.** `SUPABASE_BETA_RUN_SQL_QUERY` is still supported. Create a Supabase integration/MCP server and explicitly configure the Supabase SQL tool in that MCP server if it is not shown on the simplified Supabase MCP page. **Use the hosted API base URL for hosted Supabase.** For hosted Supabase, the base URL should be `https://api.supabase.com`. Do not use the project's own Supabase URL unless the customer is self-hosting Supabase. If the wrong base URL was used, delete/recreate the MCP config or connection with the correct base URL. **Pass a supported custom base URL for self-hosted Supabase.** Supabase tools default to hosted Supabase at `https://api.supabase.com`, while current toolkit versions can accept a base URL for self-hosted instances. If a self-hosted setup fails, verify the toolkit version and that the custom base URL is passed in the supported field. **Configure Management API scopes on the OAuth app.** Supabase configures Management API OAuth scopes on the OAuth app rather than in the authorization URL. Set the desired scopes in the customer's Supabase OAuth app, create the corresponding Composio auth config, and reconnect so the new grant applies. See Supabase's current [OAuth scope documentation](https://supabase.com/docs/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes). ## Troubleshoot Supabase permissions and rate limits [#troubleshoot-supabase-permissions-and-rate-limits] **Verify provider-side access for permission errors.** If Supabase returns a permissions/access-control error, verify the connected Supabase account has the required permissions in Supabase. These can be provider-side server permission errors rather than Composio issues. **Inspect the underlying error for rate limits.** If the customer sees a rate-limit message, capture the underlying Composio/tool/provider error rather than the wrapper agent's message, because the limit may come from the external provider or agent layer rather than a Composio service limit. --- # Tavily (/kb/guide/toolkits-tavily) ## Use COMPOSIO\_SEARCH\_TAVILY for Tavily search [#use-composio_search_tavily-for-tavily-search] Use the updated Tavily search tool slug `COMPOSIO_SEARCH_TAVILY` when invoking Tavily search through Composio. If an older Tavily search slug returns schema-related gateway errors, switch to this slug before deeper debugging. ## Initiate Tavily API-key connections in the legacy JS SDK with generic\_api\_key [#initiate-tavily-api-key-connections-in-the-legacy-js-sdk-with-generic_api_key] For Tavily API-key auth in the legacy JS SDK, list the Tavily integration with `toolset.integrations.list({ appName: "tavily" })`, then initiate the connected account with `appName: "tavily"`, `authMode: "API_KEY"`, the integration ID, and `authConfig: { generic_api_key: "" }`. This was provided as a workaround for a JS SDK issue, so prefer the current SDK flow when available. ## Use composio\_search for auth-free Exa/Tavily-style search [#use-composio_search-for-auth-free-exatavily-style-search] For auth-free web search through Composio, use the `composio_search` toolkit, which provides Exa/Tavily and other search capabilities without separate authentication. Use the standalone Tavily toolkit when a workflow specifically needs Tavily as its own provider-backed integration. --- # TikTok (/kb/guide/toolkits-tiktok) ## TikTok is supported, but customers generally need their own TikTok developer app [#tiktok-is-supported-but-customers-generally-need-their-own-tiktok-developer-app] TikTok is available as a toolkit and currently uses customer-owned TikTok developer app credentials. ## TikTok URL-prefix verification must be done on a customer-owned redirect domain, not Composio's shared callback domain [#tiktok-url-prefix-verification-must-be-done-on-a-customer-owned-redirect-domain-not-composios-shared-callback-domain] Do not host TikTok verification files on Composio's shared callback domain. TikTok URL-prefix verification is meant to prove ownership of the redirect domain. Use a redirect URI on a domain you control, host TikTok's verification file there, register that static parameter-free URI in TikTok, and then forward or proxy the callback to Composio if needed. ## TikTok OAuth uses `client_key`; credential mismatch or old `client_id` handling causes `client_key` errors [#tiktok-oauth-uses-client_key-credential-mismatch-or-old-client_id-handling-causes-client_key-errors] A TikTok `client_key` error is returned by TikTok, not Composio. First re-copy the Client Key and Client Secret from the TikTok developer app, checking for swapped values or trailing spaces. Also confirm the registered redirect URI exactly matches TikTok requirements. Historically, TikTok required `client_key` in the authorize URL while older Composio handling used `client_id`; if an older flow is involved, unshorten the redirect URL and verify the parameter shape. ## TikTok app status, scopes, and sandbox/production mode determine who can complete OAuth [#tiktok-app-status-scopes-and-sandboxproduction-mode-determine-who-can-complete-oauth] For TikTok OAuth failures, ask for the app type/status, sandbox vs production mode, enabled APIs/scopes, redirect URI, and screenshots of the OAuth screen. If the TikTok app is sandbox or under review, only authorized testers/users may be able to complete OAuth. ## Old TikTok-specific MCP URL patterns are deprecated; use Connect MCP [#old-tiktok-specific-mcp-url-patterns-are-deprecated-use-connect-mcp] Do not use old toolkit-specific MCP URL patterns for TikTok. Use Connect MCP at `connect.composio.dev/mcp` or create the appropriate MCP/server through the current dashboard/API flow. ## Public TikTok posting requires the customer's own app to pass TikTok's content posting audit [#public-tiktok-posting-requires-the-customers-own-app-to-pass-tiktoks-content-posting-audit] For TikTok public content posting, you must go through TikTok's content posting audit with your own OAuth app. Without an audited/approved app, posting may be restricted, for example to private-only visibility or limited testing behavior. ## TikTok Ads/Marketing may require a separate approved app and test credentials [#tiktok-adsmarketing-may-require-a-separate-approved-app-and-test-credentials] TikTok Ads/Marketing may require a separate approved TikTok app and active account credentials. Determine whether you need authentication only or specific tools, and allow time for TikTok app approval. ## TikTok custom auth must request only approved scopes [#tiktok-custom-auth-must-request-only-approved-scopes] The TikTok toolkit's default set can include `user.info.basic`, `user.info.profile`, `user.info.stats`, `video.list`, `video.upload`, and `video.publish`. A customer-owned app approved for only a subset can fail OAuth when the auth config falls back to the full default. Set an explicit scope list on the custom auth config containing only permissions TikTok approved for that app, then reconnect. Existing tokens retain their original grants. Tools for profile details, statistics, or video lists remain unavailable unless the corresponding scopes are approved and requested. --- # Trello (/kb/guide/toolkits-trello) Use this guide to connect Trello with OAuth1, route users through current MCP flows, and resolve Trello identities for tools and triggers. ## Connect Trello with OAuth1 [#connect-trello-with-oauth1] Use Composio-managed OAuth1 for the standard connection flow. Create a custom OAuth1 auth config when you need control over the Trello provider app, and have each user complete the authorization flow. ## Route Trello through current MCP and Connect flows [#route-trello-through-current-mcp-and-connect-flows] **Route each call to the correct user or account.** For multi-user Trello MCP usage, create the Trello auth config and have users complete the auth flow. Then route MCP calls to the right user or connection by appending `user_id=` or `connected_account_id=` to the MCP server URL, for example `/mcp?user_id=abcd`. **Use the generated MCP configuration in Cursor.** To use Trello in Cursor, create a Trello MCP instance or server in Composio, select the Trello tools, then run or add the generated MCP command or config in Cursor. Complete the Trello account connection when prompted by the MCP flow. **Migrate legacy MCP endpoints.** If you are using `https://mcp.composio.dev/trello` or another legacy Trello MCP endpoint, migrate to Tool Router or Composio Connect. Tool Router and Connect are the supported path for current integrations. ## Resolve Trello users and trigger board IDs [#resolve-trello-users-and-trigger-board-ids] **Get the authenticated Trello user.** Use `TRELLO_GET_MEMBERS_BY_ID_MEMBER` with `idMember` set to `me` to retrieve the authenticated Trello user or member for the current connection. **Validate board IDs before creating triggers.** If Trello triggers fail, verify the board ID first. Use tools such as `TRELLO_GET_ORGANIZATIONS_BOARDS_BY_ID_ORG` or `TRELLO_GET_BOARDS_BY_ID_BOARD` to retrieve or confirm the board ID, then recreate or retry the trigger with the valid board ID. --- # Twitter (/kb/guide/toolkits-twitter) Use this guide to configure Twitter/X authentication, choose the credentials each action needs, and troubleshoot developer-app or toolkit-version errors. ## Configure Twitter/X authentication [#configure-twitterx-authentication] **Use a customer-owned OAuth app.** Composio-managed credentials are not available for the Twitter toolkit. Create an app in the X Developer Portal, then create a custom Composio auth config with that app's credentials before connecting an account. This has been required since managed Twitter credentials were removed in February 2026. * [Twitter toolkit authentication details](https://docs.composio.dev/toolkits/twitter) * [Managed Twitter credentials removal](https://docs.composio.dev/docs/changelog/2026/02/12) **Match the current Composio callback exactly.** For Twitter OAuth callback mismatch errors, configure the Twitter/X developer app with the exact callback shown by the current Composio auth-config flow. Do not use a legacy v1 callback from older examples. ## Publish and search with the correct credentials [#publish-and-search-with-the-correct-credentials] **Follow X's post-length rules.** Twitter/X enforces strict post length limits. For normal posts, keep the content under 280 characters and follow X's official character-counting behavior, since URLs, Unicode, and special characters may be counted by provider-specific rules. **Use the Application Bearer Token for app-only actions.** Several X actions—including recent or full-archive search and counts, post lookup by IDs, post usage, label-stream, and compliance-job actions—use app-only authentication. They read the `Application Bearer Token` from the Twitter auth config, not the connected user's OAuth access token. If user-token actions succeed but these actions return 401, verify that the bearer token comes from the same X Developer App as the OAuth client credentials and that the app's X API plan allows the endpoint. Adding user OAuth scopes does not repair an invalid app bearer token. Reconnect only when the user grant also needs to change. ## Troubleshoot developer-app and toolkit-version errors [#troubleshoot-developer-app-and-toolkit-version-errors] **Fix `client-not-enrolled` and `App not linked to project` in the X developer app.** These errors usually mean the Twitter/X developer app is not correctly connected to a Twitter developer project, or the OAuth app configuration is stale after X's API model changes. Verify the app is linked to a project, configured according to the Twitter setup guide, and aligned with current X API requirements. If the connected account is already `EXPIRED`, recreate the connection after fixing the app configuration. **Update older toolkit versions for X v2 support.** The current Twitter/X toolkit uses v2 endpoints. If behavior looks like an older endpoint, check the toolkit version and retry on the latest available version. --- # Webflow (/kb/guide/toolkits-webflow) ## Create or update Webflow collection items with the draft/live flag [#create-or-update-webflow-collection-items-with-the-draftlive-flag] Use `WEBFLOW_CREATE_COLLECTION_ITEM` to create a collection item and set whether it is draft or live with the `is_draft` parameter. Use `WEBFLOW_UPDATE_COLLECTION_ITEM_V2` to update an existing item. If the customer specifically needs Webflow v2's dedicated individual collection-item publish/live endpoints, treat that as separate publish-collection-item support rather than the basic create/update flow. The older `WEBFLOW_UPDATE_COLLECTION_ITEM` action is deprecated. ## Use the current Webflow toolkit version for recently added page tools [#use-the-current-webflow-toolkit-version-for-recently-added-page-tools] When a recently added Webflow tool such as `WEBFLOW_GET_PAGE` is not found through the API, pass the toolkit/tool version explicitly. The base version `00000000_00` can be older than a dated release. Use the latest Webflow toolkit version shown by Composio for API calls that need newly added tools. ## Deprecated Webflow v1 endpoints caused publish-site integration failures [#deprecated-webflow-v1-endpoints-caused-publish-site-integration-failures] If Webflow calls fail because the integration is using unsupported or deprecated endpoints, check whether the failing action is an older v1 Webflow tool. Use the current `WEBFLOW_PUBLISH_SITE` action and current toolkit version; if the failure persists, share the failed tool-call log ID with Composio support. --- # WhatsApp (/kb/guide/toolkits-whatsapp) Use this guide to connect a WhatsApp Business account, configure Meta authentication, send messages, receive events, and handle coexistence onboarding. ## Connect a WhatsApp Business account [#connect-a-whatsapp-business-account] **Use a WABA-backed business account instead of a personal account.** WhatsApp API usage requires a WhatsApp Business Account. Personal WhatsApp accounts are for personal communication and are not supported by the WhatsApp Business API flows used by the toolkit. To send WhatsApp messages through Composio, use a WABA-backed business account. **Provide the WhatsApp Business Account ID.** The WABA ID, or WhatsApp Business Account ID, is required because the WhatsApp Business API needs it to identify the business account. Customers can find it in Meta Developers under the app's WhatsApp API Setup section, or fetch it programmatically by calling `GET /me/businesses` and then `GET /{business_id}/owned_whatsapp_business_accounts` with an access token. **Pass the system user token and WABA ID for API-key auth.** For WhatsApp API key auth, pass the system user token as the bearer token and pass the WABA ID as `generic_id`. The required connection fields depend on the auth scheme, so fetch the toolkit/auth-config initiation fields if unsure. Hosted auth links can also collect these values from the user instead of hardcoding them. **Pass the WABA ID as `generic_id` for OAuth2.** WhatsApp OAuth2 auth still requires `generic_id`, and that value is the WhatsApp Business Account ID. API key auth requires both `bearer_token` and `generic_id`, while OAuth2 only requires `generic_id` for initiation. Differences in required initiation fields usually come from the selected auth scheme. ## Configure Meta OAuth and app access [#configure-meta-oauth-and-app-access] **Publish a Meta developer app with the Business use case.** For WhatsApp OAuth with a customer-owned Meta app, create a Meta developer app, enable the Business use case, configure the WhatsApp product, and publish the app so users can connect to it. The Meta app/account used during connection should match the account that owns or can access the WhatsApp Business setup. **Add the Composio redirect URI to the Meta app.** For Meta OAuth apps, add the Composio redirect URI to the correct redirect/callback URI field in the Meta developer app. OAuth failures during callback can happen when the app does not allow the redirect URI used by the Composio auth config. ## Send WhatsApp messages and templates [#send-whatsapp-messages-and-templates] **Create and approve a template before sending it.** Sending a WhatsApp template message requires a template to already exist in WhatsApp/Meta. The send-template tool sends an existing template by name/language and parameters; it does not remove the need to create and approve the template first. **Use a current toolkit version for template `components`.** Support for `components` was added to the WhatsApp send-template flow in a newer toolkit version. If you cannot pass template variables/components to `WHATSAPP_SEND_TEMPLATE_MESSAGE`, upgrade to the latest WhatsApp toolkit version and verify the `components` field is available in the tool schema. **Pass real sender and recipient identifiers.** For WhatsApp send-message actions, make sure the action arguments contain the actual `phone_number_id` and recipient `to_number`. Placeholder values in the tool arguments will fail even if the connected account itself is active. ## Receive events and extend WhatsApp workflows [#receive-events-and-extend-whatsapp-workflows] **Use triggers or webhooks for replies.** WhatsApp does not expose every reply-reading flow as a normal API action in the toolkit. The better product shape is a trigger/webhook for events such as message or reply received. Where a first-party WhatsApp trigger is not available for the exact use case, TimelinesAI may be an alternative because it includes WhatsApp-related trigger support. **Use Proxy Execute for direct provider operations.** For provider API operations that are not exposed as first-class WhatsApp tools, Proxy Execute can be used with a scoped Composio API key that allows proxy execution. Use this when you need to call a Meta/WhatsApp endpoint directly while still going through Composio-managed connection context. ## Set up WhatsApp Business app coexistence [#set-up-whatsapp-business-app-coexistence] Keeping an existing WhatsApp Business app number active while also using the Cloud API is a Meta-side coexistence onboarding flow, not a Composio activation toggle. Follow Meta's [Onboard WhatsApp Business app users](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users) flow through a Solution Partner or Tech Provider that supports it. After the number is active on Cloud API, connect its WABA in Composio through the normal WhatsApp setup. For API-key auth, use the system user token as `bearer_token` and the WABA ID as `generic_id`. If the number is shown as `ON_PREMISE`, it may need Meta's On-Premises API to Cloud API migration steps before normal registration or coexistence. Route that onboarding/migration step to Meta or the customer's BSP, then help with the Composio connection once Cloud API is active. --- # Wrike (/kb/guide/toolkits-wrike) Use this guide to map Wrike users correctly, call Wrike APIs through Composio, and use current nested-folder behavior. ## Map Wrike users and assignees [#map-wrike-users-and-assignees] **Use the Wrike user ID, not the account ID.** For Wrike task update or assignment fields, pass the Wrike user `id` value rather than the `accountId`. Wrike validates the user identifier shown in the user object, not the account identifier. **Read user relationships from Wrike's ID arrays.** Wrike task data can contain several user-id fields, including `authorIds`, `responsibleIds`, `sharedIds`, and `followerIds`. For fetch-task results, use the `resolve_user_names` parameter, which is enabled by default, to return those ids along with their names. If identifying the creator specifically, check `authorIds`. **Do not expect a native `assignee` field.** Do not expect a separate `assignee` field from the Wrike tasks API or the corresponding fetch-tasks tool. Wrike represents user relationships through id arrays such as responsible/user fields instead of a top-level `assignee` field. ## Call Wrike APIs through Composio [#call-wrike-apis-through-composio] **Use the proxy endpoint or SDK `executeRequest`.** For direct Wrike API calls through an existing Composio connected account, call the Composio proxy endpoint with the Wrike path and method, for example `endpoint: "/tasks"`, `method: "GET"`, and the `connected_account_id`. In SDK code, the same pattern can be done with `toolset.client.actions.executeRequest({ connectedAccountId, endpoint: "/tasks", method: "GET", parameters: [] })`. Ensure endpoint values are quoted strings. **Pass `entityId` as a string when `getConnections` returns 404 in v3 SDK flows.** When using v3 SDK connection APIs, pass the `entityId` as a string. If the code stores the value as `enterpriseId`, pass it through the SDK entity helper, for example `.getEntity("enterpriseId")`. Also use a current v3 SDK package rather than an older release candidate. ## Use the latest Wrike toolkit version for nested folders [#use-the-latest-wrike-toolkit-version-for-nested-folders] For Wrike folder APIs, avoid the base `00000000_00` toolkit version when dealing with nested folders. Retry with `latest` so the request uses the current nested-folder pagination behavior. --- # Xero (/kb/guide/toolkits-xero) ## Xero redirect URI must match the current auth-config flow exactly [#xero-redirect-uri-must-match-the-current-auth-config-flow-exactly] Make sure the redirect URI configured in the Xero OAuth app exactly matches the URI shown by the current Composio auth-config flow. Do not fall back to a legacy v1 callback from an older example; copy the current callback from the setup UI or auth-config documentation and match it exactly, without adding a trailing slash. ## Xero OAuth app should be a Web app and the client secret must match the auth config [#xero-oauth-app-should-be-a-web-app-and-the-client-secret-must-match-the-auth-config] For Xero BYOA/custom OAuth, verify the Xero developer app is configured as a `Web app`, not `Mobile or Desktop`. The redirect URI must match exactly, and the client secret in Composio must match the current secret in the Xero developer portal. If a connection remains in `EXPIRED` with `Connection initiation did not complete within 10 minutes`, restart the auth flow and complete the Xero consent step within the 10-minute window. ## Remove deprecated Xero scopes that cause invalid-scope/CSP/login errors [#remove-deprecated-xero-scopes-that-cause-invalid-scopecsplogin-errors] Remove the deprecated/invalid Xero scopes `accounting.journals.read`, `accounting.reports.read`, `accounting.transactions`, and `accounting.transactions.read` from the auth config. Reconnect after removing them. Use Xero's current OAuth scope documentation and keep required scopes such as `offline_access`, `email`, `profile`, `openid`, and the supported `accounting.*` scopes needed for the tools. ## Connect MCP discovers Xero tools through meta-tools instead of preloading every tool [#connect-mcp-discovers-xero-tools-through-meta-tools-instead-of-preloading-every-tool] Connect MCP uses meta-tools such as `COMPOSIO_SEARCH_TOOLS` and `COMPOSIO_MULTI_EXECUTE_TOOL` to discover and execute toolkit-specific tools dynamically. For Xero, the expected flow is: ask/search for the task such as `get Xero contacts`, let the agent discover the relevant Xero tool, then execute it through the multi-execute tool. This avoids loading 1000+ tools into context up front. ## Connect MCP and Platform MCP Xero connections are independent [#connect-mcp-and-platform-mcp-xero-connections-are-independent] Connect MCP servers and Platform MCP servers are independent. A connection visible in Platform is not automatically available through Connect MCP, so confirm which surface created the Xero connection before debugging account selection. --- # YNAB authentication (/kb/guide/toolkits-ynab) ## YNAB supports managed and customer-owned OAuth [#ynab-supports-managed-and-customer-owned-oauth] Use Composio-managed OAuth for the standard connection flow. Create a custom auth config with the customer's YNAB client ID and client secret when they need control over the provider app. For custom OAuth, register the exact redirect URI shown by the current Composio auth-config flow. If YNAB reports that an application is restricted, review the YNAB app's current review and access-token restrictions. An app intended only for its owner and an app distributed to unrelated users can have different provider review requirements. Do not promise a provider approval date. --- # YouTube (/kb/guide/toolkits-youtube) Use this guide to upload YouTube videos, configure scopes and triggers, and distinguish upload failures from provider limits. ## Upload videos through current YouTube actions [#upload-videos-through-current-youtube-actions] **Use a full file path with `YOUTUBE_UPLOAD_VIDEO`.** `YOUTUBE_UPLOAD_VIDEO` is intended to be used through the SDK because it accepts `videoFilePath`. Pass a full local file path string such as `/path/to/video.mp4`, and use the latest toolkit version when debugging older upload failures. **Choose the current upload path for the file.** For YouTube video uploads, pass a local file path through SDK automatic file handling or use `YOUTUBE_MULTIPART_UPLOAD_VIDEO` when its single-request upload shape fits the file. Do not quote the old 50 MB staged-file limit without checking the current upload path and platform limit. ## Troubleshoot YouTube processing and provider limits [#troubleshoot-youtube-processing-and-provider-limits] **Inspect current execution and provider state for `processing abandoned`.** If YouTube returns `processing abandoned`, first check YouTube Studio/provider status, the video format, and the current toolkit version. Use a fresh execution log to distinguish provider processing failure from an upload-transfer failure. **Treat `uploadLimitExceeded` as a channel limit.** YouTube limits how many videos a channel can upload in a 24-hour period across the website, mobile apps, and the YouTube API. If an upload returns `uploadLimitExceeded` or YouTube shows **Daily upload limit reached**, wait 24 hours before retrying. Switching to a different OAuth app does not bypass the channel limit. See YouTube's [common uploading errors](https://support.google.com/youtube/answer/10383400). ## Configure YouTube scopes and triggers [#configure-youtube-scopes-and-triggers] **Include the caption-download scope.** For YouTube caption download, verify the connected account includes `https://www.googleapis.com/auth/youtube.force-ssl`. The scope was described as part of the default YouTube scope set, but the actual connection should still be checked from connection details when a tool call fails. **Validate the channel ID when creating a trigger.** YouTube supports triggers. For `YOUTUBE_NEW_ACTIVITY_TRIGGER`, use the field descriptions to provide the correct channel ID; trigger creation may otherwise fail without a separate preflight warning. --- # Zendesk (/kb/guide/toolkits-zendesk) Use this guide to connect a Zendesk account and discover the current tools and triggers for tickets and search. ## Connect Zendesk with the correct subdomain and auth scheme [#connect-zendesk-with-the-correct-subdomain-and-auth-scheme] **Let OAuth inject the access token automatically.** For Zendesk OAuth, the access token is injected automatically after the OAuth flow completes; customers do not need to manually enter it. Redirect URI can be optional depending on the auth-config setup, but if Zendesk requires one, configure the Composio auth redirect URL in the Zendesk OAuth app. **Pass the account subdomain, not the full URL.** Zendesk requires the account subdomain during connection initiation. Pass the Zendesk site prefix, not the full URL, as `subdomain`. Composio uses that field to construct Zendesk URLs. **Include the subdomain in OAuth config values.** When initiating a Zendesk OAuth connected account, pass `subdomain` in the connection config values. For the current SDK shape, use `config={"auth_scheme":"OAUTH2","val":{"subdomain":""}}`; older examples used `connected_account_params={"subdomain":""}`. **Pass the subdomain and encoded credential for API-key/basic auth.** For Zendesk API-key/basic auth connection initiation, pass the Zendesk `subdomain` and `basic_encoded` credential value in the connection data. The `basic_encoded` value should be the base64 encoding of the Zendesk email/token credential form requested by the auth config. ## Use current Zendesk tools and triggers [#use-current-zendesk-tools-and-triggers] **Request the latest toolkit version when listing tools.** When listing Zendesk tools through the API, include the toolkit version query parameter. For example, use `toolkit_versions=latest&toolkit_slug=zendesk&limit=1000`. Without the toolkit version query, the API response may not show the expected tool set. **Search Zendesk with the dedicated search action.** Use `ZENDESK_SEARCH_ZENDESK` for Zendesk search use cases. **Update tickets with the current ticket action.** Use `ZENDESK_UPDATE_ZENDESK_TICKET` for Zendesk ticket updates. For endpoint-level context, the corresponding Zendesk API is the Update Ticket endpoint in Zendesk's ticketing API. **Fetch known ticket details directly.** The Zendesk get-ticket-by-id action is available and returns the ticket details in a single tool call. Use it when the customer has a Zendesk ticket ID and needs the ticket's metadata/details rather than searching first. **Verify the current trigger catalog before quoting availability.** Zendesk has trigger support in Composio. Verify the current trigger catalog before naming an exact count. --- # Zoho Books (/kb/guide/toolkits-zoho-books) ## Use Zoho Invoice for create estimate [#use-zoho-invoice-for-create-estimate] `ZOHO_BOOKS_CREATE_ESTIMATE` is no longer the Zoho Books tool to use for estimates. Use the Zoho Invoice toolkit and `ZOHO_INVOICE_CREATE_ESTIMATE` instead. ## Optional Zoho Books item rate filters do not have default values [#optional-zoho-books-item-rate-filters-do-not-have-default-values] The Zoho Books item `rate` field and related rate filters are optional. Composio does not set default values for those fields; if omitted, they default to null behavior. If an agent includes `0` or another value, treat that as model/tool-call behavior and inspect the tool schema with the get-tools-by-slug API reference or adjust the agent/tool-call layer so optional rate filters are not sent unless explicitly requested. ## Pin Zoho Books toolkit version when reproducing list-items behavior [#pin-zoho-books-toolkit-version-when-reproducing-list-items-behavior] When reproducing or sharing a controlled snippet for Zoho Books list-items behavior, use `toolkit_versions={"zoho_books": "latest"}`, then request `ZOHO_BOOKS_LIST_ITEMS` explicitly for the user's connected account context. ## Zoho domain suffix parameter expects the extension value [#zoho-domain-suffix-parameter-expects-the-extension-value] For Zoho Books auth, the Zoho domain parameter expects the extension value such as `com`, `eu`, or `in`; Composio appends it into the URL as the corresponding domain suffix like `.com`. Do not include the leading dot in the parameter value. --- # Zoho Mail (/kb/guide/toolkits-zoho-mail) ## ZOHO\_MAIL\_MESSAGES\_SEND\_EMAIL supports sending attachments [#zoho_mail_messages_send_email-supports-sending-attachments] `ZOHO_MAIL_MESSAGES_SEND_EMAIL` supports sending attachments. If attachment support was previously missing, retry with the latest toolkit version. If attachment sending still fails, contact Composio support with the redacted tool-call details. ## Pass the correct Zoho region when connecting Zoho Mail [#pass-the-correct-zoho-region-when-connecting-zoho-mail] For Zoho Mail connection issues, verify the region passed during connection initiation. Zoho accounts can be region-specific, so an EU or other regional account may fail if the default/wrong region is used. Retry the connection with the correct Zoho region. ## Zoho Mail account\_id must be handled as a string to avoid JavaScript precision loss [#zoho-mail-account_id-must-be-handled-as-a-string-to-avoid-javascript-precision-loss] Treat Zoho Mail `account_id` values as strings, not integers. Zoho account IDs can exceed JavaScript's safe integer limit, and numeric coercion can silently truncate them before the tool call reaches Zoho. If you see unexpected account IDs or tool failures with long IDs, verify the schema and payload preserve `account_id` as a string. ## Connect MCP is agent-oriented; authenticate Zoho Mail in Connect dashboard before tool use [#connect-mcp-is-agent-oriented-authenticate-zoho-mail-in-connect-dashboard-before-tool-use] Connect MCP is intended for agent/client workflows through Tool Router, not as a raw direct API endpoint. For Zoho Mail, make sure the user has connected a Zoho Mail account in the Connect dashboard first, then use the supported MCP client flow. If the user wants direct API execution, route them to Tool Router/API or Proxy Execute patterns instead of treating Connect MCP as a raw REST proxy. --- # Zoho (/kb/guide/toolkits-zoho) Use this guide to connect Zoho in the correct region, choose current Zoho tools and fields, and handle pagination or large identifiers safely. ## Connect Zoho in the correct region [#connect-zoho-in-the-correct-region] **Pass the account's region as the domain extension.** Zoho requires the correct region/domain extension during connection initiation. Accepted values include `com`, `eu`, `in`, `cn`, and `au`. Pass the customer's Zoho account region, not a full URL, so Composio can build the correct `accounts.zoho.` URL. **Use `suffix.one` for the Zoho Mail domain extension.** For Zoho Mail, the expected connection initiation field can appear as `suffix.one`, displayed as Domain Extension. Pass values such as `com`, `eu`, or `in` in `config.val["suffix.one"]` when initiating the connection. **Inspect the toolkit schema for required auth and connection fields.** Use `toolkits.get("")` or the toolkit-by-slug API to inspect the full Zoho toolkit schema, including auth config creation fields and connected account initiation fields. This is the reliable way to discover region/domain fields and other required inputs. **Initiate a new OAuth2 connection for MCP setups.** Zoho uses OAuth2. For MCP setups, create an MCP config for Zoho, then initiate/connect the Zoho account through the MCP client or dashboard. If the client does not automatically start the OAuth flow, prompting it to initiate a new Zoho connection can help. ## Choose current Zoho tools and fields [#choose-current-zoho-tools-and-fields] **Use a current Zoho Mail tool version for attachments.** Attachment support was added to `ZOHO_MAIL_MESSAGES_SEND_EMAIL`. If you cannot send attachments with Zoho Mail, use a current toolkit version and verify the send-email tool schema includes attachment fields. **Create estimates through Zoho Invoice.** For creating estimates, use the `zoho_invoice` toolkit action `ZOHO_INVOICE_CREATE_ESTIMATE`; the estimate action is not exposed through the Zoho Books toolkit. **Omit optional Zoho Books fields unless they are needed.** `rate` on `ZOHO_BOOKS_LIST_ITEMS` is optional and has no default value in the schema. If an agent sends `rate: 25.5` or another value, that is coming from the model/tool-call generation, not from a Composio schema default. Prompt the model not to pass optional fields unless needed, or call the tool directly with only required arguments. **Find the lead before converting it.** For Zoho lead conversion, verify the `lead_id` first. Use `ZOHO_GET_ZOHO_RECORDS` to retrieve the lead record and obtain the correct `lead_id`, then pass that value into the conversion tool. ## Handle Zoho pagination and identifiers [#handle-zoho-pagination-and-identifiers] **Follow page tokens and provider rate limits.** Zoho list endpoints may return around 200 records per request and require pagination with `page_token` for larger result sets. Multiple tool calls may be needed, and Zoho's own API rate limits can still apply. **Treat Zoho Mail account IDs as strings.** Zoho Mail account IDs can exceed JavaScript's safe integer range, so they should be modeled and passed as strings. If a Zoho Mail tool truncates or changes a large account ID, contact Composio support with the redacted payload and log ID so `account_id` can be verified as a string throughout serialization. --- # Zoom (/kb/guide/toolkits-zoom) ## Zoom custom OAuth apps may only connect users in the app owner's Zoom organization unless configured/approved otherwise [#zoom-custom-oauth-apps-may-only-connect-users-in-the-app-owners-zoom-organization-unless-configuredapproved-otherwise] If you are using your own Zoom OAuth app, verify whether the users you are connecting belong to the same Zoom organization or whether the app is published/approved for external users. An unpublished internal app may only connect users from its own Zoom organization. ## Zoom should use the default Composio redirect URL unless the auth guide says otherwise [#zoom-should-use-the-default-composio-redirect-url-unless-the-auth-guide-says-otherwise] For Zoom OAuth setup, do not arbitrarily change the redirect URL. Use the default redirect URL/callback shown by Composio or the Zoom auth guide. If auth fails after redirect changes, recreate or update the auth config with the default redirect URL. ## `ZOOM_GET_A_MEETING_SUMMARY` needs the correct past-meeting UUID and auto summary enabled [#zoom_get_a_meeting_summary-needs-the-correct-past-meeting-uuid-and-auto-summary-enabled] For Zoom meeting summaries, verify that the meeting was created with `settings__auto_start_meeting_summary=true`. Then fetch the correct past-meeting UUID from Zoom's `/v2/past_meetings/{meetingId}/instances` endpoint and use that UUID with `ZOOM_GET_A_MEETING_SUMMARY`; the numeric meeting ID alone may not be sufficient. ## Zoom delete/summary tools may require extra scopes in a custom OAuth app [#zoom-deletesummary-tools-may-require-extra-scopes-in-a-custom-oauth-app] If a Zoom tool fails with a scope or permission issue, check whether the required scope is configured on the customer's Zoom OAuth app. `ZOOM_DELETE_A_MEETING` needs `meeting:write` or `meeting:write:admin`, while fetching past meeting instances or summary UUIDs needs `meeting:read:list_past_instances`. ## Zoom OAuth consent branding comes from the customer's OAuth app [#zoom-oauth-consent-branding-comes-from-the-customers-oauth-app] For Zoom OAuth branding, use the customer's own Zoom OAuth app. The OAuth consent screen logo/name is picked up from the OAuth app settings rather than from Composio alone. --- # Examples --- # Build a Slack bot that can do work with you and your team (/examples/general-agent-with-pi) The agent is the easy part. [Pi](https://github.com/earendil-works/pi/tree/main/packages/coding-agent) does the reasoning; Composio gives it 1000+ apps to act on. In three lines you have an agent that can open a PR, check a calendar, or search a Notion workspace for one user. The work is everything around it: putting that agent in Slack, where a whole team talks to it, and making it act as *each* person while posting as one bot. That's a handful of Composio pieces: 1. **Triggers** deliver every Slack message to your server as a webhook. 2. **Sessions** give each user their own scoped toolset, so the agent acts as *them*. 3. **A shared connection** lets the bot speak as the workspace bot, with one install for everyone. 4. **Redirected auth links** keep OAuth out of the channel: when an app isn't connected, the bot DMs the user a link and resumes on approval. 5. **The proxy** reaches the Slack Web API endpoints the toolkit doesn't wrap as tools. Below you build the whole thing from scratch: a basic agent first, then a piece at a time up to the full server, then a browse of the real source. You bring a Composio API key and an agent runtime. Composio brings the workspace. ## Setup [#setup] You need a [Composio API key](https://dashboard.composio.dev?utm_source=docs\&utm_medium=content\&utm_campaign=examples-general-agent-with-pi), a publicly reachable URL for your server, and [Bun](https://bun.sh). **No public URL? Use a Cloudflare tunnel** Composio posts webhooks to your server, so it needs a public URL. In local development, run a [Cloudflare tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) to expose your local port: ```bash cloudflared tunnel --url http://localhost:3000 ``` Use the `https://…trycloudflare.com` URL it prints as your `APP_URL`. ```bash bun add @composio/core @composio/experimental @earendil-works/pi-coding-agent ``` ## Install the bot [#install-the-bot] A Slack bot needs a Slack app to authenticate as and a stream of events. Composio gives you both, so you never register a webhook with Slack or hold a bot token. The `slackbot` toolkit ships with Composio-managed OAuth, and you install it as one **[shared connection](/docs/extending-sessions/shared-connections)** for the whole workspace. This is `install.ts`, run once, built up three steps at a time: **`install.ts` — complete file** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); // The scopes the bot needs. The slackbot toolkit ships Composio-managed OAuth, // so you never register your own Slack app. const authConfig = await composio.authConfigs.create('slackbot', { type: 'use_composio_managed_auth', name: 'workspace-bot', credentials: { scopes: ['app_mentions:read', 'channels:history', 'chat:write', 'reactions:write', 'users:read'], user_scopes: ['search:read'], }, }); // One connection for the whole workspace: authorize it as SHARED. const setup = await composio.create('setup:workspace-bot', { toolkits: ['slackbot'], authConfigs: { slackbot: authConfig.id }, manageConnections: true, }); const request = await setup.authorize('slackbot', { callbackUrl: `${process.env.APP_URL}/setup/callback`, experimental: { accountType: 'SHARED' }, }); console.log('Approve the install:', request.redirectUrl); // On the OAuth callback: open the ACL, subscribe your webhook, create triggers. // Persist connectedAccountId as SLACK_CONNECTION_ID for the bot server. export async function onSetupCallback(connectedAccountId: string) { await composio.connectedAccounts.updateAcl(connectedAccountId, { allowAllUsers: true }); await composio.triggers.setWebhookSubscription({ webhookUrl: `${process.env.APP_URL}/webhooks/composio` }); await composio.triggers.create('setup:workspace-bot', 'SLACKBOT_CHANNEL_MESSAGE_RECEIVED', { triggerConfig: { is_bot_message: false } }); await composio.triggers.create('setup:workspace-bot', 'SLACKBOT_DIRECT_MESSAGE_RECEIVED', { triggerConfig: {} }); } ``` A webhook subscription is the *pipe*; each trigger is a *tap*. Together they stream channel messages and DMs to your server. The connected account id that comes back from the OAuth callback is the `SLACK_CONNECTION_ID` the server pins into every session. ## Build the bot [#build-the-bot] `bot.ts` starts as a bare three-line agent and grows into the server, one Composio concept at a time. Each diff below is exactly what that concept adds. ### Start with a basic agent [#start-with-a-basic-agent] The whole idea, before any Slack: create a session for a user, hand the Pi provider the session so it can search and execute, and run a prompt. This already acts across every app that user has connected. ### Put it in a Slack thread [#put-it-in-a-slack-thread] Turn the one-shot agent into a handler. Each Slack thread gets its own [session](/docs/configuring-sessions), reused so the agent keeps context, and the reply goes back with the `SLACKBOT_SEND_MESSAGE` tool. The session is keyed to the Slack user, so when Alice asks for a GitHub issue it opens as *Alice*, against her GitHub connection. ### Share one workspace connection [#share-one-workspace-connection] By default a connected account is **PRIVATE**: only its creator can use it. The install authorized the Slack connection as **SHARED**, so you pin it into every session. Now Alice's session has *her* GitHub connection but *the workspace's* Slack connection. It posts as the bot, and acts everywhere else as Alice. ### Reach the gaps with the proxy [#reach-the-gaps-with-the-proxy] Most Slack actions are `SLACKBOT_*` tools. The few that aren't, like the typing indicator and opening a DM channel, drop down to `session.proxyExecute`, which calls the Slack Web API with the pinned connection's auth so you never touch a token. ### Redirect auth links [#redirect-auth-links] The payoff. When the agent reaches for an app the user hasn't connected, the tool result carries a one-time Composio connect URL. You never want it in the channel or in the model's context. The bot extracts it, **redacts** it from the tool output, DMs it to the user privately, and the run resumes the moment they approve, because the session was created with `waitForConnections`. ### Serve the webhook [#serve-the-webhook] Verify each trigger's signature with `composio.triggers.verifyWebhook`, then hand the payload to `handleSlackMessage` off the response path so a slow handler doesn't get retried. That's the whole server. ## The whole project [#the-whole-project] The two files above are the spine. The real project rounds them out with grouped auth-link DMs, per-user routing, message chunking, reaction acks, and durable storage. Here's a slice of the actual source, with the Composio touch-points highlighted. Browse the tree, read the files: > The Slack bot browser is a documentation snapshot; a public repository is not available. ## Run it [#run-it] Run `bun install.ts` once to set up the bot, start the server with `bun bot.ts`, then `@mention` the bot in any channel. It opens a session as you, finds the tool it needs, runs it against your connections, and replies in thread as the workspace bot, usually within a few seconds. Ask it to do something in an app you haven't connected yet and it DMs you a link first, then continues once you approve. - [Configuring sessions](/docs/configuring-sessions): Everything a session can scope: toolkits, tools, connections, and limits - [Shared connections](/docs/extending-sessions/shared-connections): SHARED vs PRIVATE accounts and the per-user ACL --- # Integrate Composio into an existing harness (/examples/harness-integration) Most Composio examples hand the agent a session and let Composio's [meta tools](/docs/how-composio-works#meta-tools) find, authenticate, and run tools. That default works well until you already have your own planner, retrieval index, dispatcher, and permission layer. Then you need the 1000+ apps and managed auth, not another agent inside yours. Composio sits underneath your harness in four calls: 1. **List toolkits.** [`composio.toolkits.get()`](/reference/sdk-reference/typescript/toolkits) returns the catalog independent of any user. That's your integrations directory. 2. **Connect what the user picks.** [`session.authorize(slug)`](/docs/authentication) starts the OAuth flow for this user. Bring your own OAuth credentials via [auth configs](/docs/authentication/programmatic-auth-configs) for a white-label flow, or [import existing tokens](/docs/authentication/importing-existing-connections) to migrate from another store. 3. **Fetch tools.** Load raw JSON Schema with [`composio.tools.getRawComposioTools({ toolkits })`](/docs/configuring-sessions#browsing-the-catalog), or serve the same tools over [MCP](/docs/sessions-via-mcp) and let your harness call them through the protocol. 4. **Execute by slug.** [`session.execute(slug, args)`](/docs/how-composio-works#executing-session-tools) runs one tool as the connected user and hands back data. Nothing here asks a model to make a decision. Every call is one your code makes, at a moment you choose, because your planner decided. ```mermaid `flowchart LR subgraph yours["Your harness"] planner[Planner] index[Tool index] mcpClient[MCP client] dispatch[Dispatcher] end subgraph composio["Composio"] toolkits["composio.toolkits.get()"] authorize["session.authorize()"] schemas["tools.getRawComposioTools()"] mcp["session.mcp"] execute["session.execute()"] end ui([Your settings UI]) --> toolkits toolkits --> authorize toolkits --> schemas toolkits --> mcp schemas --> index index --> planner mcp --> mcpClient mcpClient --> planner planner --> dispatch dispatch --> execute execute --> planner ` ``` > **Is this the right example for you?**: Read this one if you already own the loop: your own planner, your own tool search or retrieval, your own execution and approval path. You get the app catalog, managed auth, and a single execute call, and Composio stays out of the reasoning. Read [What is a session?](/docs/how-composio-works) instead if you'd rather Composio did the discovery. Runtime search returns guidance and recommended steps alongside schemas, keeps a shared context across calls, and costs you far less context than loading hundreds of schemas upfront. That's a real advantage to give up knowingly. ## Setup [#setup] You need a [Composio API key](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=examples-agent-harness) to list the catalog and drive sessions. Listing apps does not need a user id; creating a session and connecting accounts does. There's no model provider in this example: your harness already has one. **Python:** **TypeScript:** ## Create a session your code drives [#create-a-session-your-code-drives] A [session](/docs/how-composio-works) is the per-user context: which toolkits are in play, which accounts are connected, what may run. You still want one. What changes is that nothing on it ever reaches a model. Your harness never calls `session.tools()`, so the search and execute meta tools sit on the session unused. Two settings are still worth passing at creation. `manageConnections: false` drops the connection meta-tools, because your settings page is doing that job. [Disabling the sandbox](/docs/configuring-sessions#disabling-the-sandbox) drops the code-execution tools, because your harness runs its own code. Leave the toolkit filter open and the session can execute any tool your planner names. If you want a white-label flow, pass `authConfigs` at creation to pin the session to your own OAuth app. `session.authorize()` then sends the user through your consent screen instead of Composio's managed app. You can update those configs later, so migrating from a default app to your own app is a session patch, not a rebuild. **Python:** ```python from composio import Composio composio = Composio() # user_id is your own identifier, stable for the life of the account session = composio.sessions.create( user_id="user_123", manage_connections=False, sandbox={"enable": False}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); // userId is your own identifier, stable for the life of the account const session = await composio.create('user_123', { manageConnections: false, sandbox: { enable: false }, }); ``` Create one session per user and reuse it. Sessions persist on the server and don't expire, so store the session id on the user record and pick it back up with `composio.use(sessionId)` on later requests. Calling `create()` again just makes another session. ## 1. List the toolkits [#1-list-the-toolkits] The catalog does not belong to a user. `composio.toolkits.list()` in Python or `composio.toolkits.get()` in TypeScript returns the app list independent of any session, so your integrations page can render before anyone signs in. **Python:** ```python page = composio.toolkits.list(limit=50) for toolkit in page.items: print(f"{toolkit.slug:<20} {toolkit.name}") # page.next_cursor -> pass back as next_cursor= for the next page ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const toolkits = await composio.toolkits.get({ limit: 50 }); for (const toolkit of toolkits) { console.log(`${toolkit.slug.padEnd(20)} ${toolkit.name}`); } // pass a cursor as { cursor } to request the next page ``` Filter by category or sort alphabetically. If you also need to know whether this user has a live connection, call `session.toolkits()` with the same toolkit list; it merges the global catalog with per-user connection state. **Python:** ```python developer = composio.toolkits.list(category="developer-tools", limit=50) connected = session.toolkits(is_connected=True, limit=100) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create('user_123'); const developer = await composio.toolkits.get({ category: 'developer-tools', limit: 50 }); const connected = await session.toolkits({ isConnected: true, limit: 100 }); ``` > **Toolkits with no auth**: Toolkits where `isNoAuth` is true (search, scrapers, and similar) need no connection. They show up in the global catalog and, if you use `session.toolkits()`, come back with no `connection` object. Treat them as always available. ## 2. Connect what the user picks [#2-connect-what-the-user-picks] `session.authorize(slug)` starts the flow and returns a connection request with a redirect URL. Send the user there. Composio holds the credentials and refreshes them; no token ever lands in your code. **Python:** ```python request = session.authorize( "github", callback_url="https://your-app.com/composio/callback", ) # Redirect the user here print(request.redirect_url) # Scripts and CLIs can block; a web server should not account = request.wait_for_connection() print(f"Connected account: {account.id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create('user_123'); const request = await session.authorize('github', { callbackUrl: 'https://your-app.com/composio/callback', }); // Redirect the user here console.log(request.redirectUrl); // Scripts and CLIs can block; a web server should not const account = await request.waitForConnection(); console.log(`Connected account: ${account.id}`); ``` In a web app, don't hold a request open on `waitForConnection`. Point `callbackUrl` at your own route and, when the user lands back on it, confirm with `session.toolkits({ toolkits: ['github'], isConnected: true })`. That check is authoritative and costs one call. > **White-label OAuth and migrating apps**: By default, `session.authorize()` uses Composio's managed OAuth app. To brand the consent screen or control the OAuth client, create an auth config with your own credentials and pass it at session creation. **Python:** ```python auth_config = composio.auth_configs.create( toolkit="github", options={ "type": "use_custom_auth", "auth_scheme": "OAUTH2", "name": "My GitHub App", "credentials": { "client_id": os.environ["GITHUB_CLIENT_ID"], "client_secret": os.environ["GITHUB_CLIENT_SECRET"], "oauth_redirect_uri": "https://backend.composio.dev/api/v1/auth-apps/add", }, }, ) session = composio.sessions.create( user_id="user_123", toolkits=["github"], auth_configs={"github": auth_config.id}, manage_connections=False, sandbox={"enable": False}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('github', { type: 'use_custom_auth', authScheme: 'OAUTH2', name: 'My GitHub App', credentials: { client_id: process.env.GITHUB_CLIENT_ID!, client_secret: process.env.GITHUB_CLIENT_SECRET!, oauth_redirect_uri: 'https://backend.composio.dev/api/v1/auth-apps/add', }, }); const session = await composio.sessions.create('user_123', { toolkits: ['github'], authConfigs: { github: authConfig.id }, manageConnections: false, sandbox: { enable: false }, }); ``` To migrate users from an existing token store, create a connected account directly with `composio.connectedAccounts.initiate()` (TypeScript) or `composio.connected_accounts.initiate()` (Python). You can update `authConfigs` on the session later, so moving from the default app to your own app is a patch rather than a rebuild. ## 3. Fetch tools [#3-fetch-tools] Your harness can consume tools in two shapes. Load raw JSON Schema and call them yourself, or expose them through an MCP server and let your harness call them over the protocol. ### Option A: Raw JSON Schema [#option-a-raw-json-schema] `getRawComposioTools` returns tool definitions with no user context and no provider wrapping: a slug, a name, a description, and input parameters as plain JSON Schema. Take the toolkits you want to expose, pull their tools, and write them into whatever corpus your planner already searches. **Python:** ```python connected = session.toolkits(is_connected=True, limit=100) slugs = [toolkit.slug for toolkit in connected.items] tools = composio.tools.get_raw_composio_tools(toolkits=slugs) for tool in tools: index.add( name=tool.slug, # "GITHUB_CREATE_AN_ISSUE" description=tool.description, parameters=tool.input_parameters, # JSON Schema ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create('user_123'); declare const index: { add(entry: { name: string; description?: string; parameters?: unknown }): void; }; const connected = await session.toolkits({ isConnected: true, limit: 100 }); const slugs = connected.items.map(toolkit => toolkit.slug); const tools = await composio.tools.getRawComposioTools({ toolkits: slugs, important: false, }); for (const tool of tools) { index.add({ name: tool.slug, // "GITHUB_CREATE_AN_ISSUE" description: tool.description, parameters: tool.inputParameters, // JSON Schema }); } ``` > **Pass `important: false` to fetch every tool, not a curated subset**: In TypeScript, `getRawComposioTools({ toolkits })` applies Composio's `important` flag on your behalf and returns a hand-picked subset of each toolkit rather than all of it. Nothing errors; the list is just shorter than the toolkit, which is a bad surprise to find in an index weeks later. The auto-apply is suppressed by `important: false` and by passing any of `tools`, `tags`, `search`, or `limit`, so a call that already sets a large `limit` is getting the full list. The Python method has no `important` parameter and always returns the unfiltered list. Which you want depends on your index. Curated is a reasonable default for semantic search over a few toolkits. Full is what you want when your planner needs to name an exact tool. Fetch per toolkit rather than in one shot once a user has a lot connected. Thirty toolkits at full breadth is several thousand schemas, and you almost certainly want them cached, keyed by toolkit and refreshed on your own schedule, rather than pulled on every turn. ### Option B: MCP server [#option-b-mcp-server] Create a session with `mcp: true` and hand `session.mcp.url` (and `session.mcp.headers` if required) to any MCP client. The server exposes the same tools and executes them under the session's connected accounts, so your harness talks the protocol instead of calling `session.execute()` directly. **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS composio = Composio() session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], tools={ "gmail": { "enable": ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"], }, }, session_preset=SESSION_PRESET_DIRECT_TOOLS, mcp=True, ) print(session.mcp.url) print(session.mcp.headers) ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from '@composio/core'; const composio = new Composio(); const session = await composio.sessions.create('user_123', { toolkits: ['gmail'], tools: { gmail: { enable: ['GMAIL_FETCH_EMAILS', 'GMAIL_CREATE_EMAIL_DRAFT'], }, }, sessionPreset: SessionPreset.DIRECT_TOOLS, mcp: true, }); console.log(session.mcp.url); console.log(session.mcp.headers); ``` Wire the URL into any MCP client. Examples for OpenAI Agents, Claude Agent SDK, and Vercel AI SDK are in the [sessions via MCP guide](/docs/sessions-via-mcp). **Alternative: let the session decide which tools exist** `getRawComposioTools` reads the catalog and ignores your session's filters. If you'd rather have one source of truth, scope the session and read its tool list back instead: set `preload: { tools: 'all' }` alongside a toolkit filter, then fetch with `composio.tools.getRawToolRouterSessionTools(session.sessionId)` in TypeScript or `composio.tools.get_raw_tool_router_meta_tools(session.session_id)` in Python. You get exactly the tools the session allows, so the set you index and the set that can execute can't drift apart. The trade-off is that every change to the exposed set becomes a session update rather than a local filter, and the returned list includes any meta tools the session still has enabled. ### Keep the session in step with what you indexed [#keep-the-session-in-step-with-what-you-indexed] If you scoped the session to specific toolkits, that filter is what gates execution later. When a user connects something new, widen the session rather than creating another one. `session.update()` is a patch: fields you omit are left alone. **Python:** ```python session.update(toolkits={"enable": [*slugs, "linear"]}) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create('user_123'); declare const slugs: string[]; await session.update({ toolkits: { enable: [...slugs, 'linear'] } }); ``` A session created with no toolkit filter, as in the setup above, already allows everything and needs no update. ## 4. Execute by slug [#4-execute-by-slug] Your planner picked a tool and produced arguments. `session.execute` runs it as this user's connected account and returns the result. No model, no retry loop, no interpretation. **Python:** ```python result = session.execute( "GITHUB_CREATE_AN_ISSUE", arguments={ "owner": "ComposioHQ", "repo": "composio", "title": "Tool schemas drift between staging and prod", "body": "Filed by the harness.", }, ) if result.error: raise RuntimeError(result.error) print(result.data) print(result.log_id) # look this up in the dashboard ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create('user_123'); const result = await session.execute('GITHUB_CREATE_AN_ISSUE', { owner: 'ComposioHQ', repo: 'composio', title: 'Tool schemas drift between staging and prod', body: 'Filed by the harness.', }); if (result.error) { throw new Error(result.error); } console.log(result.data); console.log(result.logId); // look this up in the dashboard ``` Arguments go through untouched: neither SDK validates them locally, so a wrong field reaches the upstream API and comes back as a populated `error` your planner can read and correct on the next turn, not as an exception. Exceptions are for transport failures and cancellation. `logId` ties the call to its entry in the dashboard, which is the fastest way to see what Composio actually sent upstream when a response surprises you. When the thing you need isn't wrapped as a tool, [`session.proxyExecute()`](/docs/extending-sessions/proxy-execute) calls the raw API endpoint under the same connected account. ## What maps to what [#what-maps-to-what] | Your harness | Composio | | ------------------------------ | -------------------------------------------------------------------- | | Integrations directory | `composio.toolkits.get()` / `composio.toolkits.list()` | | Per-user connection state | `session.toolkits()` | | "Connect" button | `session.authorize(slug)` | | White-label OAuth app | `authConfigs` at session creation | | Tool index or retrieval corpus | `composio.tools.getRawComposioTools({ toolkits, important: false })` | | MCP tool server | `session.mcp` | | Tool dispatch | `session.execute(slug, args)` | | Per-user isolation | one session per user id | | Audit trail | `result.logId` | Your planner, context window, approval gate, and traces stay the same. Composio takes over the global app catalog, OAuth registration, token refresh, schema maintenance, and the execute path, whether you call `session.execute()` directly or through the MCP server. > **If the tool set is fixed**: A harness with a known, small tool set doesn't need step three at all. Create the session with the [direct tools preset](/docs/configuring-sessions#direct-tools-preset) and an explicit `tools` filter, and `session.tools()` hands back exactly those tools with search, multi-execute, connection management, and the sandbox all off. That's `sessionPreset: SessionPreset.DIRECT_TOOLS` in TypeScript and `session_preset=SESSION_PRESET_DIRECT_TOOLS` in Python. Worth it when the list is short and stable; the four calls above are for when the list is whatever the user connected this morning. - [Configuring sessions](/docs/configuring-sessions): Every filter a session takes: toolkits, tools, tags, auth configs, connected accounts - [What is a session?](/docs/how-composio-works): The runtime context behind all four calls, and what the meta tools do when you leave them on - [Authentication](/docs/authentication): Managed auth, your own OAuth credentials, and pre-connecting accounts - [Proxy execute](/docs/extending-sessions/proxy-execute): Call an API endpoint Composio doesn't wrap, as the connected account --- # iMessage custom toolkit with eve (/examples/imessage-agent) Most Composio toolkits call a remote API. Some capabilities only exist on one machine, and iMessage is the classic case: there's no iMessage cloud API, so the agent has to run on your Mac and drive Messages.app directly. This example builds exactly that: a terminal agent that **texts on your behalf from your own Mac** and reaches the rest of your apps (Gmail, Calendar, GitHub, Slack) through the Composio catalog in the same breath. ``` you › read my last email and text Shams a summary you › text mom i'm running 10 min late you › what did Lena and I last text about? ``` The pattern is the takeaway, not iMessage specifically. It comes together from a handful of Composio pieces: 1. **A custom toolkit** wraps local iMessage (send, contacts, history, memory) as Composio tools. 2. **In-process execution** runs each tool right on your Mac through `session.execute`, with no remote API. 3. **One session** puts those local tools on the same surface as the 1000+ app catalog. 4. **The eve provider** turns `session.tools()` into [eve](https://github.com/vercel/eve)-native tools, so `eve dev` can call them. 5. **Triggers** let an outside event wake the agent and reach your phone, with in-chat auth through `COMPOSIO_MANAGE_CONNECTIONS` for any app you haven't connected. Below you build the integration core: the custom toolkit first, then the wiring that puts it on a session, then triggers, then a browse of the relevant source. You bring a Composio API key and a Mac. Composio brings the catalog. ## Setup [#setup] You need a [Composio API key](https://dashboard.composio.dev?utm_source=docs\&utm_medium=content\&utm_campaign=examples-imessage-agent) and macOS (the iMessage tools drive Messages.app, Contacts.app, and the local `chat.db`). **Install** ```bash npm install @composio/core @composio/experimental eve ``` **Configure** ```txt title=".env.local" COMPOSIO_API_KEY=xxxxxxxxx ``` **Grant macOS permissions** (prompted on first use): Automation for Messages, Contacts access, and Full Disk Access to read `chat.db`. ## The custom toolkit [#the-custom-toolkit] A custom toolkit is a named group of custom tools. Each tool declares an input schema and an `execute` that runs locally. Here is `SEND`, which shells out to AppleScript to drive Messages.app: **`imessage/send-message.ts` — complete file** ```typescript import { experimental_createTool } from '@composio/core'; import { z } from 'zod/v3'; import { runAppleScript, SEND_SCRIPT } from './applescript'; export const sendMessage = experimental_createTool('SEND', { name: 'Send iMessage', description: 'Send an iMessage from your Mac to a phone number or iMessage email.', preload: true, inputParams: z.object({ to: z.string().describe('Phone number or iMessage email.'), text: z.string().describe('Message body to send.'), }), execute: async ({ to, text }) => { await runAppleScript(SEND_SCRIPT, [to, text]); return { sent: true, to }; }, }); ``` Group your tools into a toolkit. The full project also ships `FIND_CONTACT` (fuzzy contact lookup over Contacts.app), `READ_MESSAGES` (recent messages from `chat.db`), and `RECALL`/`REMEMBER` (per-contact memory), each built the same way: ```ts title="imessage/index.ts" // @noErrors import { experimental_createToolkit } from '@composio/core'; import { sendMessage } from './send-message'; import { findContact } from './find-contact'; import { readMessages } from './read-messages'; import { recallContact, rememberContact } from './memory-tools'; export function createImessageToolkit() { return experimental_createToolkit('IMESSAGE', { name: 'iMessage', description: "Send and read iMessages, look up contacts, and remember people, locally on the user's Mac.", tools: [sendMessage, findContact, readMessages, recallContact, rememberContact], }); } ``` The toolkit depends only on `@composio/core`, so it drops into any Composio agent. Custom tools execute in-process through `session.execute`, which is why they aren't on the MCP URL yet. ## Wire it up [#wire-it-up] Set the [eve provider](/docs/providers/eve) on the client and register the toolkit on the session. `composio.ts` grows in three steps, one Composio concept each: ### Create the client with the eve provider [#create-the-client-with-the-eve-provider] The provider is what makes `session.tools()` return eve-native tools instead of raw Composio tools. Its approval policy pauses every iMessage send before the local AppleScript runs. ### Scope a session to the user [#scope-a-session-to-the-user] `sessions.create` gives this user their own toolset, already wired to the full Composio catalog. ### Register the local toolkit [#register-the-local-toolkit] Pass the custom toolkit through `experimental.customToolkits`, and the local iMessage tools join the catalog on the same session. Now hand that session to eve. eve discovers tools from files, so `defineComposioTools(session)` returns the resolver that exposes `session.tools()`. One line: ```ts title="agent/tools/composio.ts" // @noErrors import { defineComposioTools } from '@composio/experimental/eve'; import { session } from '../../composio'; export default defineComposioTools(session); ``` ```ts title="agent/agent.ts" // @noErrors import { defineAgent } from 'eve'; export default defineAgent({ model: 'google/gemini-2.5-flash', }); ``` That's the whole integration. Run `eve dev` and talk to it. The agent can text a contact, read a thread, and act across every connected app, with auth handled in chat through `COMPOSIO_MANAGE_CONNECTIONS`. ## Extend it: triggers [#extend-it-triggers] The agent can text, so anything that can wake the agent can reach your phone. Composio **triggers** turn an external event into an agent run: subscribe to an app event, point Composio's webhook at your app, and act on each event with the same iMessage tools. For example, surface a Linear assignment and ask the user whether to inspect it. The first turn contains no issue title or body, so third-party text does not cross into the agent prompt before the user opts in: ```ts title="agent/channels/triggers.ts" // @noErrors import { defineChannel, POST } from 'eve/channels'; import { composio } from '../../composio'; export default defineChannel({ routes: [ POST('/webhook', async (req, { send }) => { const { payload: event } = await composio.triggers.parse(req, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); if (event.userId !== 'user_123' || event.triggerSlug !== 'LINEAR_ISSUE_ASSIGNED') { return new Response(null, { status: 202 }); } await send( `A verified Linear assignment event arrived. Ask whether I want to inspect it. ` + `Do not call tools in this turn. Event reference: ${event.uuid}.`, { auth: { authenticator: 'composio-webhook', principalType: 'service', principalId: event.userId, attributes: { triggerSlug: event.triggerSlug }, }, continuationToken: event.uuid, } ); return new Response(null, { status: 202 }); }), ], }); ``` Point Composio at your webhook and create the trigger, once each. Reuse the same `composio` client from `composio.ts`: ```ts title="agent/setup-triggers.ts" // @noErrors import { composio } from '../composio'; // Register the webhook URL once per project; store the returned // secret as COMPOSIO_WEBHOOK_SECRET. const subscription = await composio.triggers.setWebhookSubscription({ webhookUrl: `${process.env.APP_URL}/webhook`, }); // Use the exact slug from the Composio triggers catalog. const trigger = await composio.triggers.create('user_123', 'LINEAR_ISSUE_ASSIGNED'); console.log(`Trigger created: ${trigger.triggerId}`); ``` Run it once with `npx tsx agent/setup-triggers.ts`, and the webhook handler above takes over from there. Swap the trigger and the prompt to create another reflex. Keep the first turn content-free, then fetch third-party content only after the user asks to continue. ## More reflexes [#more-reflexes] The prompt is where each reflex earns its keep, but the webhook should not turn untrusted content into instructions. Here is the same two-step gate for Gmail: ```ts title="agent/channels/triggers.ts" // @noErrors import { defineChannel, POST } from 'eve/channels'; import { composio } from '../../composio'; export default defineChannel({ routes: [ POST('/webhook', async (req, { send }) => { const { payload: event } = await composio.triggers.parse(req, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); if (event.userId !== 'user_123' || event.triggerSlug !== 'GMAIL_NEW_GMAIL_MESSAGE') { return new Response(null, { status: 202 }); } await send( `A verified Gmail event arrived. Ask whether I want to inspect the email. ` + `Do not call tools in this turn. Event reference: ${event.uuid}.`, { auth: { authenticator: 'composio-webhook', principalType: 'service', principalId: event.userId, attributes: { triggerSlug: event.triggerSlug }, }, continuationToken: event.uuid, } ); return new Response(null, { status: 202 }); }), ], }); ``` If the user continues, the agent can fetch the email in a separate turn and require approval before any side effect. A few more patterns worth wiring up with the same gate: * **Stand-up nudge.** A calendar event ten minutes out texts you the agenda and the meeting link, pulled straight from the invite. * **Review request.** A new GitHub review request texts you the PR title and a one-line read of the diff, so you can reply "approve" from your phone. * **Money in.** A successful Stripe payment texts you the amount and the customer, no dashboard required. * **Cover for me.** A Slack mention while you're away texts a teammate and asks them to take a look. Use the exact slug from the Composio triggers catalog for each. Signature verification proves the event came through Composio; it does not make an email subject, issue title, or other third-party text trustworthy. Reject unexpected users and trigger slugs, keep external content out of the initial prompt, and require approval before side effects such as sending a message. ## Browse the project [#browse-the-project] The key files, in one place. The custom toolkit and the session wiring carry the integration, the eve provider is imported from `@composio/experimental/eve`, and the rest is local macOS glue. > The iMessage implementation is maintained in [platform-imessage](https://github.com/ComposioHQ/platform-imessage). The complete, runnable project will be published in the Composio examples repo. ## Run it [#run-it] The browser above is an implementation slice, not a standalone fixture: it omits the project's package manifest and supporting `handles`, `chat-db`, and `memory` modules. The complete runnable project will be published in the Composio examples repo. Until then, use the provider walkthrough on this page in an existing eve app and treat the iMessage source as a reference for the local toolkit. The first send or contact lookup prompts for macOS permission, and the first time the agent needs an app you haven't connected, it returns an auth link in chat through `COMPOSIO_MANAGE_CONNECTIONS`. - [eve provider](/docs/providers/eve): EveProvider, the defineComposioTools resolver, and the (ctx, next) hooks. - [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits): Build and register your own in-process Composio tools. --- # Examples (/examples) End-to-end builds that wire Composio into working agents. Each one is a complete project you can read top to bottom and run. - [General agent with Pi](/examples/general-agent-with-pi): Build a Pi + Composio agent and drop it into Slack: triggers, per-user sessions, a shared connection, redirected auth links, and the proxy. - [Daily standup bot](/examples/standup-slackbot): A Slack bot that drafts each teammate's standup from their own connected tools: your own Slack app, tool-router sessions, manual tool execution, the proxy, and per-member auth links. - [Local sandbox PR reviewer](/examples/local-sandbox-pr-reviewer): Run a PR reviewer in your own sandbox while it calls GitHub tools through a Composio session. - [iMessage custom toolkit with eve](/examples/imessage-agent): An agent that texts on your behalf from your own Mac: a custom toolkit wraps local iMessage in-process, and the eve provider puts it on the same session as the whole Composio catalog. --- # Review pull requests in a sandbox you own (/examples/local-sandbox-pr-reviewer) Composio usually runs your tools for you. A **local sandbox** is for the times you need to run them yourself: your filesystem, your shell, your security boundary. You still get [managed auth](/docs/authentication) and 1000+ apps; you just keep the code execution. This example builds a GitHub PR reviewer that does exactly that: it clones a pull request into a sandbox *you* own, runs the repo's real checks there, and posts one grounded comment. The sandbox here is E2B, but E2B is just the sample. The same pattern works with your own VM, container, Kubernetes job, or internal sandbox service. It comes down to a handful of Composio pieces: 1. **A local sandbox session** is a [Composio session](/docs/how-composio-works) with [code execution turned off](/docs/configuring-sessions#disabling-the-sandbox). Composio still does [discovery](/docs/how-composio-works#meta-tools) and auth; it just won't run code for you. 2. **The helper contract** is what comes back: a Python helper exposing the same [`run_composio_tool`, `invoke_llm`, and `web_search`](/docs/sandbox/remote) tools Composio's managed sandbox runs for you, plus the `env` it needs. You inject it into your sandbox and the agent calls it. 3. **Your sandbox is the boundary.** Tool *execution* happens in a box you control. E2B is the replaceable sample runner; the contract it honors is the real interface. > **The sandbox holds your project API key**: The `env` that `experimental_createLocalWorkbenchSession` returns includes your **project** `COMPOSIO_API_KEY`, and you inject that `env` into the sandbox. Anything running there can read it, including the untrusted PR code you clone and build. Treat the sandbox as your trust boundary: run it on infrastructure you control, give the reviewer a key scoped to only what it needs, and rotate the key if a run could have leaked it. Below you build the host orchestration from scratch: a bare client first, then a piece at a time up to the full run loop, then a browse of the real source. You bring a Composio API key and a place to run code. Composio brings the tools. ## Setup [#setup] You need a [Composio API key](https://dashboard.composio.dev?utm_source=docs\&utm_medium=content\&utm_campaign=examples-local-sandbox-pr-reviewer), an OpenAI API key for the reviewer agent, a GitHub connection for your `COMPOSIO_USER_ID`, and [Bun](https://bun.sh). **No sandbox provider? Use the E2B sample runner** The host writes the Composio helper into a sandbox and runs the agent there, so it needs *somewhere* to run code. This example ships an [E2B](https://e2b.dev) runner in `src/sandbox/e2b.ts` so you can run it today with just an `E2B_API_KEY`. E2B is a hosted sandbox provider: that key provisions an isolated microVM to run the agent in, so you don't have to stand up a VM or container yourself. It's still real infrastructure, just E2B's to manage rather than yours. E2B is deliberately isolated to that one file. To run on your own VM, container, or CI worker, replace `createE2bSandbox` with anything that honors the same contract: create a directory, write `helperSource` into it, pass `env` to the process, stream stdout and stderr back, and tear down on your schedule. ```bash bun add @composio/core @composio/experimental e2b @openai/agents ``` Connect GitHub once for the user id you'll review as, then keep that same id for the review run: ```bash bun run connect ``` ## Build the host [#build-the-host] `src/runner.ts` is the host: it owns orchestration, never tool execution. It starts as a bare Composio client and grows into the full run loop, one concept at a time. Each diff below is exactly what that concept adds. ### Create the Composio client [#create-the-composio-client] The whole thing acts as one stable user, against the connections they own. Start there. ### Check the GitHub connection [#check-the-github-connection] A local sandbox still leans on Composio for auth and [tool discovery](/docs/how-composio-works#meta-tools); only code execution moves to your side. So before booting any infrastructure, confirm this user actually has [GitHub connected](/docs/authentication), and hand them a connect link if not. ### Create the local sandbox session [#create-the-local-sandbox-session] The core of the integration. You create a [Composio session](/docs/configuring-sessions#creating-a-session) yourself with code execution off (`workbench.enable: false`, so Composio will not run code for you), then hand that session to `experimental_createLocalWorkbenchSession`. The helper validates the session is local (it errors if the session has the remote workbench enabled, because the managed workbench and a local sandbox can't both run for one session) and returns the pieces you run yourself: a `helperSource` (a Python helper with `run_composio_tool`, `invoke_llm`, and `web_search`) and the `env` that helper needs to reach Composio from inside your box. ### Start your sandbox, inject the helper [#start-your-sandbox-inject-the-helper] Boot a box you control, write `helperSource` into it as `composio_helper.py`, and pass `env` to the process. That helper is the *only* Composio-specific thing your sandbox has to carry. E2B is the sample runner; swap it for anything that honors the same contract. ### Run the reviewer and stream output [#run-the-reviewer-and-stream-output] Run the agent inside the sandbox and stream its output back. Whenever the agent calls `run_composio_tool`, the helper routes that GitHub action back through Composio under this user's connection. Tool *execution* happens in your box; discovery and auth stay managed. ## The whole project [#the-whole-project] The file above is the spine. The real project rounds it out with a CLI, a smoke/dry-run path, the E2B runner behind the sandbox contract, the reviewer agent and its review policy, and the `composio_helper.py` the helper source compiles to. Here's a slice of the actual source, with the Composio touch-points highlighted. Browse the tree, read the files: > The local PR reviewer browser is a documentation snapshot; a public repository is not available. ## Run it [#run-it] Dry-run first to validate your input with no credentials, network calls, or sandbox startup, then run it for real: ```bash bun run review -- --repo ComposioHQ/composio --pr 123 --dry-run bun run review -- --repo ComposioHQ/composio --pr 123 ``` The host opens a local sandbox session, boots the sandbox, and runs the repo's real checks inside it, then posts one grounded comment, or nothing if it can't build the PR. --- # Daily standup bot (/examples/standup-slackbot) Standup is a crucial part of running an effective engineering team, and also oh so tedious: every morning, everyone digs back through what they did and writes it up. It's worse for teams spread across timezones, where there's no shared standup to anchor the day, so it's easy to just forget. But the work you did *is* there: the PRs in GitHub, the docs in Notion, the decisions in Slack threads. If it's all recorded somewhere, an agent should be able to at least draft it. [Composio sessions](/docs/configuring-sessions) make this *incredibly easy for agents*: a session hands the agent everything it needs, [search](/docs/how-composio-works#meta-tools) to find the right tool, [parallel execute](/docs/how-composio-works#meta-tools) to run many at once, a [sandbox](/docs/sandbox/remote) and [volumes](/docs/sandbox/remote#files-and-mounts). It uses Composio to extract, parse, and cross-reference data across all those sources and write clean summaries of the real work your team shipped. All you have to do is create a session for your teammate and let it cook. ![The daily standup reminder in Slack](/images/standup-slackbot/slack-reminder.png) *The daily reminder, with Draft and Connect more tools buttons* ![A generated standup draft in Slack](/images/standup-slackbot/slack-draft.png) *The draft the agent writes, delivered to a teammate in Slack* So we built a Slack bot that does exactly that. Once a day, at a set time in each teammate's own timezone, it reminds them to post in the daily standup thread in a central channel. With one button click, they can run a subagent that uses their Composio connections to generate a clean, consolidated draft to review and post. We'll build it step by step. > **Is this the right example for you?**: This is a deliberately advanced, opinionated build. It's a strong reference for five things: * **Background-agent sessions**: the draft agent runs on a schedule, not in a conversation. It works from the tools a member already connected and never pauses to ask for auth. * **Manual execution for deterministic workflows**: outside the draft, the bot doesn't let an agent decide. It runs a fixed flow, calling tools directly with [manual execution](/docs/tools-direct/executing-tools), so a button always triggers the same exact steps. * **Manual, pre-connected auth**: members connect their tools ahead of time using [manual connections](/docs/authentication/manually-authenticating), and the agent just uses whatever is there. * **White-labelling** (advanced): your own Slack app and bot identity, via [white-labelling](/docs/authentication/white-labeling-authentication). This is *not* the easy path. We'd recommend Composio's managed apps, which require no additional configuration. Only do this if you specifically want your own branding. * **The proxy** (advanced): using [`proxyExecute`](/docs/extending-sessions/proxy-execute) to call Slack API endpoints Composio doesn't wrap as tools. It is **not** an example of [in-chat or dynamic auth](/docs/authentication) (asking a user to connect a tool mid-run), and it's more setup than many bots need. If you'd rather have a Slack bot with zero setup (Composio's managed app) or in-chat auth, start with the [general Slack bot](/examples/general-agent-with-pi) instead. The Slack bot itself follows a deterministic flow: the same menu every day. When a member taps a button, it launches a subagent with a Composio session to produce the draft. Here's the shape of it: ```mermaid `flowchart TD cron([Vercel cron]) --> thread[Find or create today's thread] thread --> dm dm@{ img: "/images/standup-slackbot/slack-reminder.png", label: "DM each due member: Draft or Connect", pos: "b", w: 300, h: 69, constraint: "on" } dm -->|Draft| agent[Create a Composio session for the user and launch a sub-agent to generate summary] agent --> review review@{ img: "/images/standup-slackbot/slack-draft.png", label: "Member reviews: Confirm or Edit", pos: "b", w: 300, h: 143, constraint: "on" } review -->|Confirm| post[Post into the thread as the member] dm -->|Connect| oauth oauth@{ img: "/images/standup-slackbot/slack-connect.png", label: "Creates buttons for the user to link their accounts to Composio via OAuth", pos: "b", w: 340, h: 58, constraint: "on" } click agent "/docs/how-composio-works#meta-tools" "Composio metatools" ` ``` ## Setup [#setup] You need a [Composio API key](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=examples-standup-slackbot), a Slack workspace you can install an app into, and Node with [tsx](https://nodejs.org). The finished bot deploys to [Vercel](https://vercel.com) as two serverless functions, a cron and an interactivity handler, so there's no long-running server. ## Make your custom Slack bot [#make-your-custom-slack-bot] This bot doesn't post as "Composio". It posts as *my* app, with its own name, icon, and (frankly ridiculous) face: ![The Daily Standup Bot avatar](/images/standup-slackbot/bot-avatar.png) *Create the app from scratch and name it* **Add the Bot Token Scopes.** Under **OAuth & Permissions**, add: `chat:write`, `im:write`, `channels:history`, `channels:read`, `users:read`, `users:read.email`, `team:read`. Then turn on **Interactivity** and point its Request URL at your deployment's `/api/interactivity`. ![Adding bot token scopes under OAuth & Permissions](/images/standup-slackbot/bot-scopes.png) *Add the bot token scopes* **Grab the app credentials.** On **Basic Information**, copy the **Client ID** and **Client Secret**. Composio drives the OAuth as your app with these. ![The app's Client ID and Secret under Basic Information](/images/standup-slackbot/app-credentials.png) *Copy the Client ID and Secret* ## Auth the bot [#auth-the-bot] The Slack app exists; now connect it through Composio so your code can act as it. You create one `slackbot` auth config from your credentials, then a setup script does the OAuth once with Composio's [manual authentication](/docs/authentication/manually-authenticating) flow. > **`slackbot` vs `slack`**: Composio has two Slack toolkits, and this bot uses both: * **`slackbot`** authenticates a Slack *app* and acts as the **bot** (a bot token). It posts the reminders and drafts as "Daily Standup Bot," and it's the one you white-label here. * **`slack`** authenticates an individual **user** and acts as *them* (a user token). Each teammate connects this so the bot can post their standup under their own name and read their activity for context. Rule of thumb: posting *as the bot* uses `slackbot`; doing something *as a person* uses `slack`. **Create an auth config and pick the `Slackbot` toolkit.** In the [Composio dashboard](https://dashboard.composio.dev/~/project/auth-configs?utm_source=docs\&utm_medium=content\&utm_campaign=examples-standup-slackbot), click **Create Auth Config** and search `slackbot`. Choose **Slackbot**, *not* `Slack`: `Slackbot` posts as the bot identity, while `Slack` acts as an individual user. ![Choosing the Slackbot toolkit, not Slack](/images/standup-slackbot/auth-config-slackbot.png) *Pick Slackbot, not Slack* **Use your own credentials.** Pick **OAuth 2.0**, then **Your Own Credentials**, and paste the Client ID and Secret from before. Add `team:read` to the user token scopes. This is the white-label step: your app, your name, your face. ![Selecting Your Own Credentials and entering the Client ID and Secret](/images/standup-slackbot/auth-config-credentials.png) *Use your own credentials* **Save the auth config id.** Once created, copy its `ac_...` id into `COMPOSIO_SLACKBOT_AUTH_CONFIG_ID`. This is the one auth config your app uses to take actions on behalf of your bot. ![The created slackbot auth config with its ac_ id](/images/standup-slackbot/auth-config-created.png) *The created auth config and its ac_ id* **Run the setup script to connect the bot.** For this bot, we first need to connect the bot itself to Composio, which only needs to be done once. The script creates an OAuth link for you to connect your *Slack bot* to Composio, which lets you use Composio to send messages on behalf of your bot. **`scripts/setup.ts` — complete file** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const AUTH_CONFIG = process.env.COMPOSIO_SLACKBOT_AUTH_CONFIG_ID!; // Connect the bot's own Slack app once, so it can post and DM as the bot. async function main() { const session = await composio.create('default', { authConfigs: { slackbot: AUTH_CONFIG }, }); const toolkits = await session.toolkits({ toolkits: ['slackbot'] }); const active = toolkits.items.find((t) => t.slug === 'slackbot')?.connection?.isActive; if (active) { console.log('Bot already connected.'); return; } // Not connected: print the Connect Link, then wait for the user to finish. const connectionRequest = await session.authorize('slackbot'); console.log('Authorize the bot:', connectionRequest.redirectUrl); const account = await connectionRequest.waitForConnection(); console.log('Bot connected:', account.id); } main(); ``` The first run prints a link and waits: ```text ╭─────────────────────────────────────────────────────────╮ │ Daily Standup Bot: One-Time Setup │ ╰─────────────────────────────────────────────────────────╯ ✅ Auth config has the required user scopes. · Bot is not connected yet. Generating an authorization link… Open this URL in your browser to authorize the bot: https://backend.composio.dev/s/AbC123xy Waiting for you to complete the OAuth flow (Ctrl+C to abort)… ✅ Bot connected to Slack. ────────────────────────────────────────────────────────────────────── 🎉 Setup complete. Invite the bot to your standup channel and point your Slack app's Interactivity Request URL at https:///api/interactivity ────────────────────────────────────────────────────────────────────── ``` Open that link to approve the bot, and the connection goes live: ![Approving the bot's OAuth connection](/images/standup-slackbot/oauth-approve.png) *Approve the bot in Slack* ![Composio successfully connected to Slackbot](/images/standup-slackbot/connected.png) *Connected* The script is idempotent and repeatable. Forgot a scope, or hit an issue? No stress, just re-run it with `--reconnect`. ## Talk to Slack [#talk-to-slack] To send and update messages in our deterministic bot workflow, we use Composio's `SLACKBOT_SEND_MESSAGE` and `SLACKBOT_UPDATES_A_MESSAGE` tools via [manual tool execution](/docs/tools-direct/executing-tools). `SLACKBOT_SEND_MESSAGE` takes Block Kit `blocks`, so a message with interactive buttons can go through a tool too. When a Slack action has no tool, like opening a modal (`views.open`), it drops to [`proxyExecute`](/docs/extending-sessions/proxy-execute): the escape hatch for anything the named tools don't cover, hitting any Slack Web API endpoint as a connected account with no token in your code. ## Make the buttons work [#make-the-buttons-work] Our StandUp bot gives the user two options every morning: **Draft** or **Connect more tools**. Each message uses [Block Kit](https://api.slack.com/block-kit) to create those buttons. For each button we define an `action_id` that lets us recognise which button was clicked. ```ts declare const memberEmail: string, dmChannel: string, dmTs: string; // the reminder's Draft button const draftButton = { type: 'button', style: 'primary', text: { type: 'plain_text', text: '📝 Draft' }, action_id: 'draft', value: JSON.stringify({ memberEmail, dmChannel, dmTs }), }; ``` ![The daily standup reminder in Slack](/images/standup-slackbot/slack-reminder.png) *The daily reminder, with Draft and Connect more tools buttons* When it's clicked, Slack POSTs to your `/api/interactivity` handler. Verify the request, ack within Slack's 3-second window, then route on the `action_id`: **`api/interactivity.ts` — complete file** ```typescript import { verifySlackSignature, readRawBody, updateMessage, postAsMember } from './_utils/slack'; import { generateDraft } from './_utils/agent'; import { draftMessage, connectMenu } from './_utils/blocks'; type SlackInteractionPayload = { actions?: Array<{ action_id?: string; value?: string; }>; }; // Slack POSTs here every time someone clicks a button. Verify it really came // from Slack, then ack within 3 seconds (Slack retries if you're slow). export default async function handler(req: Request, res: Response) { const body = await readRawBody(req); if (!verifySlackSignature(body, req.headers)) return res.status(401).end(); const payload = JSON.parse(new URLSearchParams(body).get('payload') ?? '{}'); res.status(200).end(); // ack immediately await handleClick(payload); // then do the slow work } // Each button carried its context in `value`, so the handler knows exactly what // to do. No model decides anything here: the flow is fixed. async function handleClick(payload: SlackInteractionPayload) { const action = payload.actions?.[0]; const ctx = JSON.parse(action?.value ?? '{}'); if (action?.action_id === 'draft') { const draft = await generateDraft(ctx.memberEmail); // launch the subagent await updateMessage(ctx.dmChannel, ctx.dmTs, draftMessage(draft, ctx)); } else if (action?.action_id === 'connect') { await updateMessage(ctx.dmChannel, ctx.dmTs, connectMenu(ctx)); } else if (action?.action_id === 'confirm') { await postAsMember(ctx.memberEmail, ctx.channel, ctx.draft, ctx.threadTs); } } ``` **Connect more tools** generates a per-member OAuth link for each toolkit the member hasn't connected, so they can add a source without leaving Slack: ![The connect-more-tools menu in Slack](/images/standup-slackbot/slack-connect.png) *Connect more tools, each button a per-member OAuth link* **Edit** opens a modal (`views.open` through the proxy), and **Confirm** posts the draft into the day's thread as the member. ## Draft the standup [#draft-the-standup] Now this is the cool and magical part, and the easy part: all the background agent needs is a tool-router session and a prompt. When a member taps **Draft**, you spin up a session scoped to the toolkit catalogue and let the agent research and write. ### A session writes the draft [#a-session-writes-the-draft] A [tool-router session](/docs/configuring-sessions) gives the agent its tools. Pass the member's email and your full list of toolkits, hand the tools to the model, and let it investigate and write. You don't have to check which ones the member set up: the session only exposes tools for the accounts they've actually connected, and ignores the rest. ### Use what's connected, nothing more [#use-whats-connected-nothing-more] The router can also *manage* connections, asking the user to authorize any toolkits they haven't connected yet. During a draft you don't want that: if the agent reaches for a tool the member hasn't connected, it should skip it, not prompt them to log in. `manageConnections: false` removes those meta-tools, so the agent drafts from exactly what's already connected. The bot posts the result back as a draft the member can confirm or edit: ![A generated standup draft in Slack with Confirm and Edit buttons](/images/standup-slackbot/slack-draft.png) *The draft the agent writes, delivered to a teammate in Slack* ## The whole project [#the-whole-project] > The standup bot browser is a documentation snapshot; a public repository is not available. ## Run it [#run-it] Edit `standup.config.ts` with your team (each member's Slack email and timezone, plus your channel and GitHub org), set your four environment variables, run `npx tsx scripts/setup.ts` once to connect your bot, then `vercel deploy`. - [Configuring sessions](/docs/configuring-sessions): What a session can scope: toolkits, tools, connections, and connection management - [White-labeling authentication](/docs/authentication/white-labeling-authentication): Ship a bot under your own app's name, icon, and credentials - [Custom vs managed auth](/docs/authentication/custom-app-vs-managed-app): Bring-your-own Slack app versus a Composio-managed connection - [Triggers](/docs/triggers): Run agents in response to events: schedules, webhooks, and app activity --- # API Reference --- # Errors (/reference/errors) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. Composio uses conventional HTTP response codes to indicate the success or failure of an API request. In general: codes in the `2xx` range indicate success, codes in the `4xx` range indicate an error with the information provided, and codes in the `5xx` range indicate an error with Composio's servers. ## The error object [#the-error-object] ```json { "error": { "message": "No connected account found for this user and toolkit", "status": 400, "request_id": "req_abc123def456", "suggested_fix": "Connect the user to the toolkit first" } } ``` ### Attributes [#attributes] | Attribute | Description | | --------------- | --------------------------------------------------------------------------- | | `message` | A human-readable message providing details about the error. | | `status` | The HTTP status code. | | `request_id` | A unique identifier for this request. Include this when contacting support. | | `suggested_fix` | When available, guidance on how to resolve the error. | ## HTTP status codes [#http-status-codes] | Code | Status | Description | | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------ | | 200 | OK | Everything worked as expected. | | 400 | Bad Request | The request was unacceptable, often due to missing a required parameter. | | 401 | Unauthorized | No valid API key provided. | | 403 | Forbidden | The API key doesn't have permissions to perform the request. | | 404 | Not Found | The requested resource doesn't exist. | | 409 | Conflict | The request conflicts with another request (perhaps due to using the same idempotent key). | | 422 | Unprocessable Entity | The request was valid but cannot be processed. | | 429 | Too Many Requests | Too many requests hit the API too quickly. We recommend an exponential backoff of your requests. | | 500, 502, 503, 504 | Server Errors | Something went wrong on Composio's end. | ## Error types [#error-types] ### Authentication errors [#authentication-errors] Composio uses two types of API keys: * **Project API key** (`x-api-key`) — For project-level operations * **Organization API key** (`x-org-api-key`) — For organization-level access across projects | Error | Cause | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Invalid API key | The API key is incorrect or revoked. Verify in [Settings](https://dashboard.composio.dev/~/project/settings/api-keys). | | No authentication provided | The request is missing the `x-api-key` or `x-org-api-key` header. | | Invalid organization key | The organization API key is incorrect or revoked. Verify in Organization Settings. | | Insufficient permissions | The API key doesn't have access to this resource. | > See [Authenticating users](/docs/authentication) for more help. ### Tool errors [#tool-errors] Errors that occur when fetching or executing tools. | Error | Cause | | --------------------- | ------------------------------------------------------------------------------------------ | | Tool not found | The tool slug doesn't exist. Tool slugs are case-sensitive and use `SCREAMING_SNAKE_CASE`. | | No connected account | The user hasn't connected to this toolkit yet. | | Tool execution failed | The external service returned an error. Check tool parameters and user permissions. | > See [Tools and toolkits](/docs/how-composio-works) for more help. ### Connection errors [#connection-errors] Errors related to connected accounts. | Error | Cause | | --------------------------- | ---------------------------------------------------------------- | | Connected account not found | The `connectedAccountId` doesn't exist or was deleted. | | Auth refresh required | The OAuth token has expired. Prompt the user to re-authenticate. | | Connected account deleted | The connection was removed. Create a new connection. | ### Trigger errors [#trigger-errors] Errors related to trigger subscriptions. | Error | Cause | | ------------------------ | -------------------------------------------------------------- | | Trigger not found | The trigger slug doesn't exist for this toolkit. | | Trigger instance deleted | The trigger subscription or its connected account was removed. | > See [Triggers](/docs/triggers) for more help. ## Rate limiting [#rate-limiting] When you hit rate limits, you'll receive a `429` status code. See [Rate Limits](/reference/rate-limits) for details on limits by plan and best practices for handling rate limit errors. ## Getting help [#getting-help] When contacting support, include the `request_id` from the error response. - [Discord](https://discord.com/channels/1170785031560646836/1268871288156323901): Community support - [Email](mailto:support@composio.dev): Contact support team - [GitHub](https://github.com/ComposioHQ/composio/issues/new?labels=bug): Report a bug --- # Glossary (/reference/glossary) ### Auth Config A blueprint that defines how authentication works for a toolkit: the auth method (`OAUTH2`, `API_KEY`, `BEARER_TOKEN`, or `BASIC`), scopes, and credentials. A [session](/docs/how-composio-works) creates one automatically when it needs one. To use your own OAuth credentials or non-default scopes, [create a custom one](/docs/auth-configuration/custom-auth-configs). ### Auth Scheme The authentication method an auth config uses, such as `OAUTH2`, `API_KEY`, `BEARER_TOKEN`, or `BASIC`. ### Callback URL The URL a user returns to after completing an OAuth flow through a Connect Link. You pass it as `callbackUrl` when you initiate authentication. ### Composio API Key A project-scoped secret that authenticates your SDK and API requests. Every resource you create with it is scoped to that project. ### Composio Managed Auth The default mode in which Composio supplies its own OAuth app credentials for each toolkit. It requires no setup. ### Connect Link A hosted page where a user authorizes access to a toolkit. It is returned as a `redirect_url` from `session.authorize()` or `connectedAccounts.link()`, and Composio manages the full OAuth flow. See [Authentication](/docs/authentication). ### Connected Account A stored set of credentials (OAuth tokens or API keys) linked to a userID, created when a user authenticates with a toolkit. Composio refreshes OAuth tokens automatically, and a user can have [multiple connected accounts](/docs/authentication/managing-multiple-connected-accounts) for the same toolkit. IDs are prefixed `ca_`. ### Connection Request The object returned when you initiate authentication. It contains the Connect Link URL and a `waitForConnection()` method that resolves once the user completes the flow. ### Custom Tool A tool you define yourself and use alongside Composio's built-in tools. Add local experimental custom tools and custom toolkits through [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits). ### In-Chat Authentication A flow in which the agent handles authentication itself by calling `COMPOSIO_MANAGE_CONNECTIONS` to generate a Connect Link and send it to the user in the conversation. See [In-chat authentication](/docs/authentication#in-chat-authentication). ### MCP (Model Context Protocol) An open protocol for connecting AI models to external tools. Create a session with `mcp: true` to expose `session.mcp.url` and `session.mcp.headers`, an MCP-compatible endpoint any MCP client can connect to. See [Using sessions via MCP](/docs/sessions-via-mcp). ### Manual Authentication Authenticating users from your own code with `session.authorize()` or `connectedAccounts.link()`, rather than letting the agent handle it through in-chat authentication. See [Manual authentication](/docs/authentication/manually-authenticating). ### Meta Tools A set of tools included in every session: `COMPOSIO_SEARCH_TOOLS`, `COMPOSIO_GET_TOOL_SCHEMAS`, `COMPOSIO_MANAGE_CONNECTIONS`, `COMPOSIO_MULTI_EXECUTE_TOOL`, `COMPOSIO_REMOTE_WORKBENCH`, and `COMPOSIO_REMOTE_BASH_TOOL`. They let the agent discover tools, manage auth, execute in parallel, and run code without loading hundreds of tool definitions upfront. See [Meta Tools Reference](/toolkits/meta-tools). ### Modifiers Middleware that transforms tool behavior. [Schema modifiers](/docs/tools-direct/modify-tool-behavior/schema-modifiers) change a tool's schema before the agent sees it, [before-execution modifiers](/docs/tools-direct/modify-tool-behavior/before-execution-modifiers) change arguments before a tool runs, and [after-execution modifiers](/docs/tools-direct/modify-tool-behavior/after-execution-modifiers) transform the result. In Python, [`@before_file_upload`](/docs/tools-direct/modify-tool-behavior/before-execution-modifiers#before-file-upload-python) intercepts local paths for `file_uploadable` parameters before read and upload. ### Native Tools Tools you access through provider packages with `session.tools()` and call directly, rather than over MCP. Both paths give the agent the same capabilities, but tools called directly integrate with your AI framework and support modifiers and custom tools. ### Organization The top-level Composio account entity. It contains team members and projects. ### Organization API Key A key (`x-org-api-key`) for organization-level operations such as creating and managing projects. It is distinct from the project-scoped Composio API Key. ### Project An isolated environment within an organization that scopes API keys, connected accounts, auth configs, and webhooks. Resources in one project are inaccessible from another. IDs are prefixed `proj_`. See [Projects](/reference/api-reference/projects). ### Proxy Execute Making authenticated HTTP requests through a toolkit's connected account without a predefined tool. Use it for API endpoints Composio has no built-in tool for. ### Provider An adapter package that transforms Composio tools into the format an AI framework expects (OpenAI, Anthropic, LangChain, Vercel AI SDK, and others). See [Providers](/docs/providers). ### Session An ephemeral, immutable configuration object returned by `composio.create(userId)`. It ties together a userID, the available toolkits, an auth config, and connected accounts, and it exposes `tools()`, `authorize()`, and `toolkits()` (plus `mcp.url` when created with `mcp: true`). See [What is a session?](/docs/how-composio-works). ### Session ID The unique identifier for a session. Meta tools use it internally to share context across calls within the same session. ### Tool An individual action an agent can execute. It has an input schema and an output schema, and is named `{TOOLKIT}_{ACTION}` (for example, `GITHUB_CREATE_ISSUE`). ### Tool Slug A tool's unique identifier, in the `{TOOLKIT}_{ACTION}` pattern (for example, `GITHUB_CREATE_ISSUE`). ### Toolkit A collection of related tools for a single external service. Users connect to a toolkit through authentication, and all of its tools execute with the user's credentials. ### Toolkit Slug The lowercase identifier for a toolkit (for example, `github`, `gmail`, or `slack`). Use it when configuring sessions, fetching tools, or creating triggers. ### Toolkit Versioning Pinning a toolkit to a specific version so your integration keeps a consistent set of tools even as Composio updates its definitions. See [Toolkit versioning](/docs/tools-direct/toolkit-versioning). ### Trigger A source that sends structured payloads to your application when something happens in a connected app. Triggers come in two kinds: realtime (the provider pushes events the moment they happen, for example Slack, GitHub, or Asana) and polling (Composio checks the provider on a schedule, for example Gmail). Either way, events arrive at your subscription or webhook URL. See [Triggers](/docs/triggers). ### Trigger Instance A specific, active trigger scoped to a user's connected account. ### Webhook Endpoint The ingress URL Composio issues per OAuth app for webhook triggers, plus the signing secret used to verify each inbound request. Composio configures it for you in most cases. When a trigger type's `requires_webhook_endpoint_setup` flag is `true`, you configure it yourself once per OAuth app through the [Webhook Endpoints API](/reference/api-reference/webhook-endpoints). See [Custom OAuth webhooks](/docs/setting-up-triggers/custom-oauth-webhooks). ### Webhook Subscription The URL Composio delivers signed events to in your application. There is one per project, configured through the dashboard or the [Webhook Subscriptions API](/reference/api-reference/webhook-subscriptions). See [Subscribing to events](/docs/setting-up-triggers/subscribing-to-events). ### userID An identifier from your application that Composio uses to scope connected accounts, tool executions, and authorizations. Connections are fully isolated between userIDs. See [What is a session?](/docs/how-composio-works). ### White-Labeling Customizing the auth experience so users see your brand during the OAuth flow. You provide your own OAuth credentials, redirect URIs, and branding. See [White-labeling authentication](/docs/authentication/white-labeling-authentication). ### Sandbox A persistent Python environment (previously called the workbench) exposed through the `COMPOSIO_REMOTE_WORKBENCH` meta tool. Its state persists across calls within a session, which makes it useful for bulk operations, data transformations, and processing large tool responses. Configure it with the `sandbox` session key; `workbench` still works as an alias. See [Sandbox](/docs/sandbox/remote). --- # Overview (/reference) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. Composio powers tool discovery, execution, authentication, and context management for your AI agents with 1000+ toolkits. This reference covers our REST APIs and SDKs. ## Quick Reference [#quick-reference] * **Base URL**: `https://backend.composio.dev/api/v3.1` * **[Authenticating to Composio](/reference/authenticating-to-composio)**: `x-api-key` (project) or `x-org-api-key` (organization) header * **[Rate Limits](/reference/rate-limits)**: 2K-10K requests per minute (plan-dependent) ## REST API [#rest-api] | API | Description | | ----------------------------------------------------------------- | ------------------------------------------------------------- | | [Tool Router](/reference/api-reference/tool-router) | Session-based API for AI agents to discover and execute tools | | [Tools](/reference/api-reference/tools) | List, search, and execute individual actions | | [Connected Accounts](/reference/api-reference/connected-accounts) | Manage user OAuth connections to apps | | [Auth Configs](/reference/api-reference/auth-configs) | Configure how users authenticate to toolkits | | [Triggers](/reference/api-reference/triggers) | Subscribe to webhooks from connected apps | | [Toolkits](/reference/api-reference/toolkits) | Browse available apps and their tools | ## SDK Reference [#sdk-reference] - [TypeScript SDK](/reference/sdk-reference/typescript): TypeScript SDK reference - [Python SDK](/reference/sdk-reference/python): Python SDK reference --- # Rate Limits (/reference/rate-limits) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. Composio enforces rate limits **per organization** over a fixed one-minute window. Every authenticated endpoint draws from the same budget — tool execution, connected accounts, triggers, and the rest — so the limit below is your organization's total across all API calls. ## Rate limits by plan [#rate-limits-by-plan] | Plan | Rate limit | Window | | ---------- | --------------- | -------- | | Hobby | 2,000 requests | 1 minute | | Pro | 10,000 requests | 1 minute | | Enterprise | Custom | - | ## Rate limit headers [#rate-limit-headers] Every response includes headers so you can track usage without guessing: | Header | Description | | ------------------------- | ------------------------------------------------------- | | `X-RateLimit` | Total requests allowed in the current window | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `X-RateLimit-Window-Size` | Window size (e.g., `60s` for 60 seconds) | | `Retry-After` | Seconds until the window resets (only on 429 responses) | ## Rate limit response [#rate-limit-response] When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: ```json { "message": "Rate limit exceeded. Limit: 10000 requests per 1 minutes" } ``` ## Best practices [#best-practices] 1. **Watch `X-RateLimit-Remaining`** — read it on each response to know how much headroom you have left in the window. 2. **Honor `Retry-After`** — on a `429`, wait the number of seconds it gives you before retrying instead of hammering the endpoint. 3. **Cache what doesn't change** — keep tool definitions and other static data client-side so you don't spend requests re-fetching them. ## Need higher limits? [#need-higher-limits] If you hit these limits regularly, upgrade your plan or [talk to us](https://calendly.com/composiohq/enterprise) about custom limits for your use case. - [Errors](/reference/errors): Understanding API error responses - [Pricing](https://composio.dev/pricing): Compare plans and limits --- # Overview (/reference/authenticating-to-composio) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. Every Composio API request authenticates with an API key. Send the key in a request header and Composio resolves it to your project or organization. Composio has three kinds of API key. They share the same authentication flow — they differ in what they can reach. | Key | Header | Scope | | -------------------------------------------------------------------------- | --------------- | ------------------------------------------------- | | Project API key | `x-api-key` | Full access to a single project. | | Organization API key | `x-org-api-key` | Access across every project in your organization. | | Scoped project API key **· New** | `x-api-key` | A chosen subset of a single project's resources. | ## Project API key [#project-api-key] A project API key authenticates to one project with full access. Use it for most application code. Get it from the dashboard: sign in to [composio.dev](https://composio.dev), open **Settings → Project Settings**, and copy the key from the **API Keys** section. Send it in the `x-api-key` header: ```bash curl https://backend.composio.dev/api/v3.1/tools \ -H "x-api-key: $COMPOSIO_API_KEY" ``` ## Organization API key [#organization-api-key] An organization API key authenticates across every project in your organization. Use it for organization-level endpoints. Get it from the dashboard: open **Organization Settings → General Settings** and copy a token under **Organization Access Tokens**. Send it in the `x-org-api-key` header: ```bash curl https://backend.composio.dev/api/v3.1/org/projects \ -H "x-org-api-key: $COMPOSIO_ORG_API_KEY" ``` ## Scoped project API key [#scoped-project-api-key] A scoped project API key authenticates to a single project but reaches only the resources you grant it — for example, executing tools without managing connected accounts. It uses the same `x-api-key` header as a default project key. Scope a key to the least it needs, then send it like any project key: ```bash curl https://backend.composio.dev/api/v3.1/tools/execute/HACKERNEWS_GET_USER \ -H "x-api-key: $COMPOSIO_SCOPED_API_KEY" \ -H "Content-Type: application/json" \ -d '{"arguments": {"username": "pg"}}' ``` See [Scoped project API keys](/reference/authenticating-to-composio/project-api-key-permissions) for the permission areas, access levels, and the routes each one covers. - [Scoped project API keys](/reference/authenticating-to-composio/project-api-key-permissions): Permission areas, access levels, and covered routes - [Errors](/reference/errors): Understanding API error responses - [Rate Limits](/reference/rate-limits): API rate limits by plan --- # Scoped Project API Key (/reference/authenticating-to-composio/project-api-key-permissions) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. [Video: Scoped Project API Key walkthrough](https://youtube.com/watch?v=ySMu9lljkWg) A scoped project API key lets you choose which project resources the key can access. Reach for one when a key needs only a subset of your project, such as executing tools, reading logs, or managing connected accounts. > You pick a key's permissions when you create it, and they can't be changed afterward. To adjust them, create a new key and rotate your application to use it. > Default project API keys keep full project API key access. Scoped keys use the permission areas and access levels on this page. ## Create a scoped API key [#create-a-scoped-api-key] Create a scoped key from the dashboard: Go to the [Composio Dashboard](https://dashboard.composio.dev). Select **Platform**. Select your project. Go to **Settings**. Open the **API Keys** tab. Click **Create API Key**, then choose the permission areas and access levels below. ## Access levels [#access-levels] | Access level | What it allows | | -------------- | ------------------------------------------------------------------- | | No access | The key cannot use routes in that permission area. | | Read only | The key can use read routes in that permission area. | | Write only | The key can use write routes in that permission area. | | Read and write | The key can use both read and write routes in that permission area. | Some read routes use `POST` because the request body carries filters or lookup input. The access level is based on what the route does, not only the HTTP method. When v3 and v3.1 expose the same route shape, this page lists one representative version instead of repeating both. Version-specific routes are listed separately. ## Permission areas [#permission-areas] Jump to each permission area to see the routes it covers. | Permission area | Available levels | Routes | | ------------------ | ------------------------------------------------ | ---------------------------------- | | Auth configs | No access, Read only, Write only, Read and write | [View routes](#auth-configs) | | Connected accounts | No access, Read only, Write only, Read and write | [View routes](#connected-accounts) | | Tools | No access, Read only | [View routes](#tools) | | Tool execution | No access, Write only | [View routes](#tool-execution) | | Proxy execute | No access, Write only | [View routes](#proxy-execute) | | Toolkits | No access, Read only, Write only, Read and write | [View routes](#toolkits) | | Triggers | No access, Read only, Write only, Read and write | [View routes](#triggers) | | Webhooks | No access, Read only, Write only, Read and write | [View routes](#webhooks) | | Observability | No access, Read only | [View routes](#observability) | | Sessions | No access, Read only, Write only, Read and write | [View routes](#sessions) | ## Auth configs [#auth-configs] View and modify auth configs. | Access | Method | Endpoint | | ------ | -------- | ---------------------------------------- | | Read | `GET` | `/api/v3/auth_configs` | | Read | `GET` | `/api/v3/auth_configs/{nanoid}` | | Write | `POST` | `/api/v3/auth_configs` | | Write | `PATCH` | `/api/v3/auth_configs/{nanoid}` | | Write | `DELETE` | `/api/v3/auth_configs/{nanoid}` | | Write | `PATCH` | `/api/v3/auth_configs/{nanoid}/{status}` | ## Connected accounts [#connected-accounts] View and manage connected accounts. | Access | Method | Endpoint | | ------ | -------- | ---------------------------------------------- | | Read | `GET` | `/api/v3/connected_accounts` | | Read | `GET` | `/api/v3/connected_accounts/{nanoid}` | | Write | `POST` | `/api/v3/connected_accounts` | | Write | `POST` | `/api/v3/connected_accounts/link` | | Write | `PATCH` | `/api/v3/connected_accounts/{nanoid}` | | Write | `PATCH` | `/api/v3/connected_accounts/{nanoid}/status` | | Write | `POST` | `/api/v3/connected_accounts/{nanoid}/refresh` | | Write | `DELETE` | `/api/v3/connected_accounts/{nanoid}` | | Write | `POST` | `/api/v3.1/connected_accounts/{nanoid}/revoke` | ## Tools [#tools] View tool definitions, inputs, scopes, and versions. | Access | Method | Endpoint | | ------ | ------ | ---------------------------------------------- | | Read | `GET` | `/api/v3.1/tools` | | Read | `GET` | `/api/v3.1/tools/enum` | | Read | `GET` | `/api/v3.1/tools/{tool_slug}` | | Read | `GET` | `/api/v3/tools/{tool_slug}/get_latest_version` | | Read | `GET` | `/api/v3.1/tools/scopes/required` | | Read | `GET` | `/api/v3.1/tools/get_scopes_required` | | Read | `POST` | `/api/v3.1/tools/execute/{tool_slug}/input` | ## Tool execution [#tool-execution] Execute predefined Composio tools. | Access | Method | Endpoint | | ------ | ------ | ------------------------------------- | | Write | `POST` | `/api/v3.1/tools/execute/{tool_slug}` | | Write | `POST` | `/api/v3/files/upload/request` | | Write | `POST` | `/api/v3/files/upload/response` | | Write | `GET` | `/api/v3/files/list` | ## Proxy execute [#proxy-execute] Execute raw proxy requests against connected accounts. Proxy execute is separate from tool execution. Grant it only when your application needs to call a connected account API through the raw proxy path. | Access | Method | Endpoint | | ------ | ------ | -------------------------------------------------------- | | Write | `POST` | `/api/v3.1/tools/execute/proxy` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/proxy_execute` | ## Toolkits [#toolkits] View and install toolkits. | Access | Method | Endpoint | | ------ | ------ | ----------------------------- | | Read | `GET` | `/api/v3/toolkits` | | Read | `GET` | `/api/v3/toolkits/{slug}` | | Read | `GET` | `/api/v3/toolkits/categories` | | Read | `GET` | `/api/v3/toolkits/changelog` | | Write | `POST` | `/api/v3/toolkits/multi` | ## Triggers [#triggers] View trigger types, manage trigger instances, and subscribe to trigger events. The realtime routes are called by the SDK (`triggers.subscribe()`) and the CLI to receive trigger events. | Access | Method | Endpoint | | ------ | -------- | ---------------------------------------------- | | Read | `GET` | `/api/v3/triggers_types` | | Read | `GET` | `/api/v3/triggers_types/{slug}` | | Read | `GET` | `/api/v3/triggers_types/list/enum` | | Read | `GET` | `/api/v3/trigger_instances/active` | | Read | `GET` | `/api/v3/cli/realtime/credentials` | | Read | `POST` | `/api/v3/cli/realtime/auth` | | Read | `GET` | `/api/v3/internal/sdk/realtime/credentials` | | Read | `POST` | `/api/v3/internal/sdk/realtime/auth` | | Write | `POST` | `/api/v3/trigger_instances/{slug}/upsert` | | Write | `PATCH` | `/api/v3/trigger_instances/manage/{triggerId}` | | Write | `DELETE` | `/api/v3/trigger_instances/manage/{triggerId}` | ## Webhooks [#webhooks] View and manage webhook endpoints and subscriptions. | Access | Method | Endpoint | | ------ | -------- | -------------------------------------------------- | | Read | `GET` | `/api/v3/webhook_endpoints` | | Read | `GET` | `/api/v3/webhook_endpoints/{nano_id}` | | Read | `GET` | `/api/v3/webhook_endpoints/schema` | | Read | `GET` | `/api/v3/webhook_subscriptions` | | Read | `GET` | `/api/v3/webhook_subscriptions/{id}` | | Read | `GET` | `/api/v3/webhook_subscriptions/event_types` | | Write | `POST` | `/api/v3/webhook_endpoints` | | Write | `POST` | `/api/v3/webhook_endpoints/{nano_id}` | | Write | `PATCH` | `/api/v3/webhook_endpoints/{nano_id}` | | Write | `DELETE` | `/api/v3/webhook_endpoints/{nano_id}` | | Write | `POST` | `/api/v3/webhook_subscriptions` | | Write | `PATCH` | `/api/v3/webhook_subscriptions/{id}` | | Write | `DELETE` | `/api/v3/webhook_subscriptions/{id}` | | Write | `POST` | `/api/v3/webhook_subscriptions/{id}/rotate_secret` | ## Observability [#observability] View execution logs and project usage summaries. | Access | Method | Endpoint | | ------ | ------ | --------------------------------------- | | Read | `POST` | `/api/v3.1/logs/tool_execution` | | Read | `GET` | `/api/v3.1/logs/tool_execution/{id}` | | Read | `POST` | `/api/v3.1/project/usage/{entity_type}` | | Read | `POST` | `/api/v3.1/project/usage/summary` | ## Sessions [#sessions] Create and operate sessions and MCP servers. This permission area covers MCP server management, the MCP runtime transport, and the tool router MCP transport. | Access | Method | Endpoint | | ------ | -------- | ------------------------------------------------------------------------- | | Read | `GET` | `/api/v3/mcp/servers` | | Read | `GET` | `/api/v3/mcp/{id}` | | Read | `GET` | `/api/v3/mcp/app/{app_key}` | | Read | `GET` | `/api/v3/mcp/servers/{server_id}/instances` | | Read | `GET` | `/tool_router/{session_id}/mcp` | | Read | `GET` | `/api/v3.1/tool_router/session/{session_id}` | | Read | `GET` | `/api/v3/tool_router/session/{session_id}/toolkits` | | Read | `GET` | `/api/v3.1/tool_router/session/{session_id}/tools` | | Read | `GET` | `/api/v3/tool_router/session/{session_id}/mounts/{mount_id}/items` | | Read | `GET` | `/api/v3.1/tool_router/session/{session_id}/config_history` | | Write | `POST` | `/api/v3/mcp/servers` | | Write | `POST` | `/api/v3/mcp/servers/generate` | | Write | `POST` | `/api/v3/mcp/servers/custom` | | Write | `PATCH` | `/api/v3/mcp/{id}` | | Write | `DELETE` | `/api/v3/mcp/{id}` | | Write | `POST` | `/api/v3/mcp/servers/{server_id}/instances` | | Write | `DELETE` | `/api/v3/mcp/servers/{server_id}/instances/{instance_id}` | | Write | `POST` | `/api/v3/mcp/{server_id}/{transport}` | | Write | `DELETE` | `/api/v3/mcp/{server_id}/{transport}` | | Write | `POST` | `/tool_router/{session_id}/mcp` | | Write | `DELETE` | `/tool_router/{session_id}/mcp` | | Write | `POST` | `/api/v3.1/tool_router/session` | | Write | `POST` | `/api/v3.1/tool_router/session/{session_id}/execute` | | Write | `POST` | `/api/v3.1/tool_router/session/{session_id}/execute_meta` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/link` | | Write | `POST` | `/api/v3.1/tool_router/session/{session_id}/search` | | Write | `PATCH` | `/api/v3.1/tool_router/session/{session_id}` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/mounts/{mount_id}/upload_url` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/mounts/{mount_id}/download_url` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/mounts/{mount_id}/delete` | | Write | `POST` | `/api/v3.1/tool_router/session/{session_id}/attach` | ## What to read next [#what-to-read-next] - [Authenticating to Composio](/reference/authenticating-to-composio): Authenticate API requests with project and organization API keys - [Projects](/reference/api-reference/projects): Understand projects, project API keys, and project-scoped resources - [Proxy execute](/reference/api-reference/tools): Call connected account APIs through the raw proxy path - [Observability](/reference/api-reference/logs): Inspect tool execution logs and usage summaries --- # API Keys (/reference/api-reference/api-keys) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Do not edit directly. */} API key management ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/api_key_revocation` | [Publicly revoke leaked Composio API keys](/reference/api-reference/api-keys/postApiKeyRevocation) | --- # Connected Accounts (/reference/api-reference/connected-accounts) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/connected-accounts.mdx, not this file. */} A connected account is a single user's authorized connection to a toolkit. It stores their credentials (OAuth tokens or API keys) and links them to your user ID, so your tools can act on that user's behalf. Composio creates a connected account when a user completes the flow defined by an [auth config](/reference/api-reference/auth-configs). From there you manage its full lifecycle: * **Create or link**: start a new connection, or generate an auth link session for the user to authorize. See [manually authenticating users](/docs/authentication/manually-authenticating). * **Refresh**: renew authentication for an account whose tokens have expired. * **Enable, disable, or update**: change an account's status or metadata without removing it. * **Revoke or delete**: revoke the grant at the provider, or remove the account from Composio. Each account is addressed by its `nanoid`. List endpoints accept filters so you can find accounts by user, toolkit, or auth config. ## Link auth (Composio Connect Links) [#link-auth-composio-connect-links] A Composio Connect Link is a hosted, secure sign-in page. You create one with the create auth link session endpoint, redirect the user to the returned URL, and Composio handles the rest: the user signs in, Composio creates the connected account, and Composio stores and refreshes its tokens. Credentials never pass through your app. This works for all Composio managed connections, with no OAuth credentials to set up. By default a connected account is `PRIVATE` and usable only by its owning user. Mark one `SHARED` to let other users reach it through a per-connection access control list. See [shared connections](/docs/extending-sessions/shared-connections). These endpoints use your project API key in the `x-api-key` header. > Shared-connection ACL fields are experimental and nested under an `experimental` block on the wire. Pin a specific SDK version if you depend on the current shape. ## Callback identity verification [#callback-identity-verification] Anyone who opens a Connect Link and consents becomes the account attached to that flow. On its own that is exploitable: someone starts a connection under their own user, copies the authorization URL before consenting, and gets a different person to finish it, attaching that person's provider account under the attacker's identity. This is OAuth session fixation. Callback identity verification defends against it by confirming the returning user before a connection activates. Set a verifier URL on a project, and Composio holds every OAuth connection there until your server confirms who came back. After the provider callback, Composio redirects the browser to the endpoint you host with one query parameter, `session_uri`, which carries no connection id, user id, or toolkit name. From your server, authenticating with your project API key, you post the `session_uri` and the signed-in `user_id` to the [complete auth endpoint](/reference/api-reference/connected-accounts/postConnectedAccountsCompleteAuth). On a match the connection activates and returns its `connected_account_id` and `toolkit_slug`; a `200` comes back only once it is `ACTIVE`. Only a `user_id` that doesn't match fails the connection: the request returns `400`, and the connection moves to `FAILED` with its `status_reason` set to `Callback identity verification failed`. Any other error leaves the connection pending, so you can restart the flow. The `session_uri` is single-use and valid for ten minutes; redeeming it spends the session whatever the outcome, so a repeat call returns `404`. Your endpoint owns the redirect from here: once you redeem the session, send the user onward yourself. A `callback_url` set when you create the connection is not used. Turn it on in the dashboard under **Settings → General → Configuration**. The URL must be public HTTPS, and Composio rejects private and reserved addresses on save, so local development needs a tunnel. Verification is opt-in per project, and once set it covers every OAuth connection in the project that goes through a provider redirect, whether you start it from the API or a Connect Link. > Connections you start from the dashboard can't complete while a verifier URL is set. A dashboard connection is owned by a Composio dashboard user, which isn't one of your app's users and isn't disclosed to your endpoint, so your app can't report a matching `user_id`. Test from your own app, or clear the verifier URL while you work in the dashboard. ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/connected_accounts/{nanoid}/revoke` | [Revoke a connected account at the provider](/reference/api-reference/connected-accounts/postConnectedAccountsByNanoidRevoke) | | `POST` | `/api/v3.1/connected_accounts/complete_auth` | [Complete a deferred OAuth connection after identity verification](/reference/api-reference/connected-accounts/postConnectedAccountsCompleteAuth) | | `GET` | `/api/v3.1/connected_accounts` | [List connected accounts with optional filters](/reference/api-reference/connected-accounts/getConnectedAccounts) | | `POST` | `/api/v3.1/connected_accounts` | [Create a new connected account](/reference/api-reference/connected-accounts/postConnectedAccounts) | | `GET` | `/api/v3.1/connected_accounts/{nanoid}` | [Get connected account details by ID](/reference/api-reference/connected-accounts/getConnectedAccountsByNanoid) | | `DELETE` | `/api/v3.1/connected_accounts/{nanoid}` | [Delete a connected account](/reference/api-reference/connected-accounts/deleteConnectedAccountsByNanoid) | | `PATCH` | `/api/v3.1/connected_accounts/{nanoid}` | [Update a connected account](/reference/api-reference/connected-accounts/patchConnectedAccountsByNanoid) | | `PATCH` | `/api/v3.1/connected_accounts/{nanoId}/status` | [Enable or disable a connected account](/reference/api-reference/connected-accounts/patchConnectedAccountsByNanoIdStatus) | | `POST` | `/api/v3.1/connected_accounts/{nanoid}/refresh` | [Re-initiate authentication for a connected account (DEPRECATED) (Legacy)](/reference/api-reference/connected-accounts/postConnectedAccountsByNanoidRefresh) | | `POST` | `/api/v3.1/connected_accounts/link` | [Create a new auth link session](/reference/api-reference/connected-accounts/postConnectedAccountsLink) | --- # Auth Configs (/reference/api-reference/auth-configs) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/auth-configs.mdx, not this file. */} An auth config is a blueprint that defines how a toolkit authenticates across all your users. It specifies the authentication method, the scopes your tools can request, and which credentials Composio uses to run the OAuth or token flow. A single auth config applies to every user who connects that toolkit. When a user authenticates against it, Composio creates a [connected account](/reference/api-reference/connected-accounts) that stores their tokens and links them to your user ID. Each auth config defines: * **Auth scheme**: OAuth2, API key, Bearer token, or Basic Auth * **Scopes**: what your tools are allowed to do on the user's behalf * **Credentials**: Composio's managed app, or your own OAuth client and secrets Reach for a custom auth config when you need your own branding on consent screens, custom scopes, a dedicated rate-limit quota, or a custom toolkit instance. See [managed vs custom auth](/docs/authentication/custom-app-vs-managed-app) for the decision and [how Composio handles authentication](/docs/authentication) for the full picture. ## Auth schemes [#auth-schemes] The `auth_scheme` on an auth config determines how users authenticate to the toolkit. Composio supports four. The schemes available for a given toolkit come from the toolkit itself. | Scheme | What it is | When it's used | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OAUTH2` | OAuth 2.0 authorization-code flow. The user authorizes through a hosted consent screen, and Composio stores and automatically refreshes the access and refresh tokens. | Most apps with user accounts (Gmail, GitHub, Slack, Notion, and so on). Uses Composio's managed OAuth app by default; bring your own for custom branding or scopes. | | `API_KEY` | A static API key the user provides. There's no OAuth flow: the key is stored on the connected account and sent on each request. | Services that authenticate with a key, such as SendGrid, Tavily, or PostHog. | | `BEARER_TOKEN` | A bearer access token you already hold (for example, from your own OAuth flow). Composio sends it as `Authorization: Bearer ` and does not refresh it, so you keep it current. | Bringing an existing OAuth or server-to-server token into Composio, or apps that issue long-lived tokens. | | `BASIC` | HTTP Basic authentication with a username and password. | Services that use Basic Auth. | Most OAuth toolkits work out of the box with Composio managed auth. For the others you supply the credential fields. To choose or customize the scheme, see [managed vs custom auth](/docs/authentication/custom-app-vs-managed-app). These endpoints use your project API key in the `x-api-key` header. Each auth config is addressed by its `nanoid`, and you can enable or disable one without deleting it. ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/auth_configs` | [Create new authentication configuration](/reference/api-reference/auth-configs/postAuthConfigs) | | `GET` | `/api/v3.1/auth_configs` | [List authentication configurations with optional filters](/reference/api-reference/auth-configs/getAuthConfigs) | | `GET` | `/api/v3.1/auth_configs/{nanoid}` | [Get single authentication configuration by ID](/reference/api-reference/auth-configs/getAuthConfigsByNanoid) | | `PATCH` | `/api/v3.1/auth_configs/{nanoid}` | [Update an authentication configuration](/reference/api-reference/auth-configs/patchAuthConfigsByNanoid) | | `DELETE` | `/api/v3.1/auth_configs/{nanoid}` | [Delete an authentication configuration](/reference/api-reference/auth-configs/deleteAuthConfigsByNanoid) | | `PATCH` | `/api/v3.1/auth_configs/{nanoid}/{status}` | [Enable or disable an authentication configuration](/reference/api-reference/auth-configs/patchAuthConfigsByNanoidByStatus) | --- # Logs (/reference/api-reference/logs) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/logs.mdx, not this file. */} The Logs API returns **individual tool execution events**, one record per tool call. Use it to debug failures, inspect request/response payloads, and trace specific user activity. For aggregated counts (how many tool calls happened), use the [Usage API](/reference/api-reference/organization) instead. All endpoints in this section require a **project API key** (`x-api-key`) or a valid session cookie. ## List logs [#list-logs] ```bash curl -X POST https://backend.composio.dev/api/v3.1/logs/tool_execution \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "limit": 20, "time_range": { "from": 1744848000000, "to": 1744934400000 }, "filters": [ { "field": "toolkit_slug", "operator": "==", "value": "gmail" }, { "field": "status", "operator": "==", "value": "failed" } ] }' ``` The response contains a page of log entries and a `next_cursor`: ```json { "logs": [ { "id": "log_-jRTWClpBoVo", "timestamp": "2026-04-17T10:25:00.000Z", "type": "tool.execution", "status": "failed", "level": "error", "message": "GMAIL_SEND_EMAIL failed: invalid recipient", "metadata": { /* tool, toolkit, user_id, connected_account_id, ... */ }, "metrics": { "duration_ms": 202 }, "parent": null } ], "next_cursor": "eyJwYWdlIjoyfQ==" } ``` Pass `next_cursor` back as `cursor` on the next request to paginate. When `next_cursor` is `null`, you've reached the end. ### Filter fields [#filter-fields] Pass one or more filters in the `filters` array. Filters are **AND**-combined. | Field | What it matches | | ---------------------- | ----------------------------------------------------------- | | `tool_slug` | The specific tool that was called (e.g. `GMAIL_SEND_EMAIL`) | | `toolkit_slug` | The toolkit (e.g. `gmail`, `slack`, `github`) | | `connected_account_id` | The connected account used for the call | | `auth_config_id` | The auth config (integration) behind the connected account | | `status` | `success` or `failed` | | `user_id` | Entity that initiated the call | | `session_id` | Tool router session, if routed through a session | | `sandbox_id` | Sandbox the call ran in, if applicable | | `request_id` | Request ID (useful for correlating with your own logs) | | `log_id` | Exact log ID (equivalent to the detail endpoint) | ### Operators [#operators] | Operator | Meaning | | -------------- | ------------------------ | | `==` | Exact match | | `!=` | Not equal | | `contains` | Substring match | | `not_contains` | Substring does not match | ### Parameters [#parameters] | Field | Type | Default | Notes | | ----------------- | -------------- | ------- | ---------------------------------------------- | | `limit` | number | `20` | Max 100 | | `cursor` | string \| null | `null` | Opaque pagination token from previous response | | `filters` | array | `[]` | AND-combined | | `time_range.from` | number | — | Epoch milliseconds | | `time_range.to` | number | — | Epoch milliseconds | ## Get a single log [#get-a-single-log] Fetch one log by ID to get the **full** payload, including request/response bodies, timing breakdowns, and source metadata: ```bash curl https://backend.composio.dev/api/v3.1/logs/tool_execution/log_-jRTWClpBoVo \ -H "x-api-key: YOUR_PROJECT_API_KEY" ``` The detail response includes everything from the list shape plus: * `timings`: `start_time` and `end_time` in epoch ms * `context`: `session_id`, `trace_id`, `request_id` * `source`: `host` (e.g. `mcp`, `sdk`, `api`), `framework`, `language` * `data`: the full request payload and response body This is the endpoint to call when you need to reconstruct *exactly* what happened, for example when debugging a 500 from a user report. ## Recipes [#recipes] ### Find failed Gmail tool calls in the last hour [#find-failed-gmail-tool-calls-in-the-last-hour] ```bash NOW=$(date +%s)000 HOUR_AGO=$(( $(date +%s) - 3600 ))000 curl -X POST https://backend.composio.dev/api/v3.1/logs/tool_execution \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"time_range\": { \"from\": ${HOUR_AGO}, \"to\": ${NOW} }, \"filters\": [ { \"field\": \"toolkit_slug\", \"operator\": \"==\", \"value\": \"gmail\" }, { \"field\": \"status\", \"operator\": \"==\", \"value\": \"failed\" } ] }" ``` ### Get failures for a specific user [#get-failures-for-a-specific-user] ```bash curl -X POST https://backend.composio.dev/api/v3.1/logs/tool_execution \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filters": [ { "field": "user_id", "operator": "==", "value": "user_abc123" }, { "field": "status", "operator": "==", "value": "failed" } ] }' ``` ### Fetch a single log's full request/response [#fetch-a-single-logs-full-requestresponse] ```bash curl https://backend.composio.dev/api/v3.1/logs/tool_execution/log_-jRTWClpBoVo \ -H "x-api-key: YOUR_PROJECT_API_KEY" ``` ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/logs/tool_execution` | [Search and retrieve tool execution logs](/reference/api-reference/logs/postLogsToolExecution) | | `GET` | `/api/v3.1/logs/tool_execution/{id}` | [Get log details by ID](/reference/api-reference/logs/getLogsToolExecutionById) | --- # Files (/reference/api-reference/files) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/files.mdx, not this file. */} These endpoints handle files that tools read and write during execution. When a tool produces or consumes a file, Composio stores it in object storage and exchanges it through presigned URLs rather than streaming bytes through the API. You reach for these endpoints to: * **List files** that tools have generated, optionally filtered by app and action. * **Request an upload**: get a presigned S3 URL, `PUT` your file to it, then pass the returned reference into a tool's input. This keeps large payloads out of request bodies. Tools receive a file reference and resolve the underlying object on their side. > File uploads are a two-step flow. Call the upload-request endpoint to mint a presigned URL, then upload the file contents directly to that URL. The API never receives the raw bytes. If your agent works with files inside a session, prefer the session file mount, where the sandbox exposes uploaded files to running code. See the [remote sandbox](/docs/sandbox/remote) for the sandbox helpers (`upload_local_file`, `smart_file_extract`) that build on this storage. These endpoints use your project API key in the `x-api-key` header. ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `GET` | `/api/v3.1/files/list` | [List files with optional app and action filters (DEPRECATED) (Legacy)](/reference/api-reference/files/getFilesList) | | `POST` | `/api/v3.1/files/upload/request` | [Create presigned URL for request file upload to S3](/reference/api-reference/files/postFilesUploadRequest) | --- # Organization (/reference/api-reference/organization) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/organization.mdx, not this file. */} The Usage API returns **aggregated counts** of tool calls and sessions. Use it to power billing dashboards, customer-facing analytics, or internal utilization reports. For individual events, use the [Logs API](/reference/api-reference/logs). There are two query shapes: * **Summary**: totals across one or more entity types in a time window. * **Breakdown**: one entity type, grouped by a dimension (tool, user, session, etc.). Each shape comes in an org-scoped and a project-scoped flavor. The org-scoped endpoints are documented below; the project-scoped usage endpoints (`POST /api/v3.1/project/usage/*`) also appear on the [Projects](/reference/api-reference/projects) reference page. ## Authentication [#authentication] | Endpoint | Header | Scope | | -------------------------------------------- | ---------------------------------------- | ------------------------ | | `POST /api/v3.1/org/usage/summary` | `x-org-api-key` *(or org JWT)* | All projects in your org | | `POST /api/v3.1/org/usage/{entity_type}` | `x-org-api-key` *(or org JWT)* | All projects in your org | | `POST /api/v3.1/project/usage/summary` | `x-api-key` *(or cookie)* | Single project | | `POST /api/v3.1/project/usage/{entity_type}` | `x-api-key` *(or cookie)* | Single project | The org endpoints accept a `project_id` filter so you can slice by project without rotating keys. ## Entity types [#entity-types] | Entity type | What it counts | | ------------ | ------------------------------------------- | | `tool_calls` | Every tool execution (successful or failed) | | `sessions` | Sessions created | ## Summary [#summary] Totals across entity types for a time window. ```bash curl -X POST https://backend.composio.dev/api/v3.1/project/usage/summary \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": 1744848000000, "to": 1744934400000, "entity_types": ["tool_calls", "sessions"] }' ``` Response: ```json { "entities": { "tool_calls": { "unit": "count", "total_quantity": "142", "event_count": 142 }, "sessions": { "unit": "count", "total_quantity": "8", "event_count": 8 } } } ``` ### Summary parameters [#summary-parameters] | Field | Type | Default | Notes | | -------------------- | ------------------- | ----------- | -------------------------------------------------------------- | | `from` | number | 30 days ago | Epoch milliseconds | | `to` | number | now | Epoch milliseconds | | `entity_types` | string\[] | all | Subset of `tool_calls`, `sessions` | | `filters.user_id` | string \| string\[] | — | Filter events by initiating user | | `filters.session_id` | string \| string\[] | — | Filter events by session | | `filters.project_id` | string \| string\[] | — | Only meaningful on org endpoints; ignored on project endpoints | ## Breakdown [#breakdown] One entity type, grouped by a dimension. Useful for answering "top N" questions. ```bash curl -X POST https://backend.composio.dev/api/v3.1/project/usage/tool_calls \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": 1744848000000, "to": 1744934400000, "group_by": "toolkit_slug", "order_by": "total_quantity", "order_direction": "desc", "limit": 10 }' ``` Response: ```json { "entity_type": "tool_calls", "unit": "count", "total_quantity": "142", "event_count": 142, "groups": [ { "key": "github", "total_quantity": "80", "event_count": 80 }, { "key": "slack", "total_quantity": "62", "event_count": 62 } ] } ``` ### Breakdown `group_by` options [#breakdown-group_by-options] | Entity | Org scope | Project scope | Default | | ------------ | ------------------------------------------------------------------------------------------ | ----------------------- | ----------- | | `tool_calls` | `tool_slug`, `toolkit_slug`, `connected_account_id`, `user_id`, `session_id`, `project_id` | same minus `project_id` | `tool_slug` | | `sessions` | `user_id`, `project_id` | `user_id` | `user_id` | ### Breakdown parameters [#breakdown-parameters] | Field | Type | Default | Notes | | ----------------- | ------------------- | ---------------- | --------------------------------------------- | | `from` | number | 30 days ago | Epoch ms | | `to` | number | now | Epoch ms | | `group_by` | string | see table | Dimension to group by | | `order_by` | string | `total_quantity` | One of `key`, `total_quantity`, `event_count` | | `order_direction` | `"asc"` \| `"desc"` | `"desc"` | | | `limit` | number | 50 | Max groups returned | | `filters` | object | — | See Filters below | ## Filters [#filters] Filters live in a `filters` object on the request body. Each filter value can be a **single string** or an **array of strings**: * Within a single field, values are OR-combined (`user_id: ["a", "b"]` matches events for user *a or b*). * Across fields, filters are AND-combined. ```json { "filters": { "user_id": ["user_123", "user_456"], "session_id": "sess_abc" } } ``` The `project_id` filter is only meaningful on the org-scoped endpoints. Project endpoints accept the field but ignore it (your key already pins the scope to a single project). ## Time ranges [#time-ranges] * `from` and `to` are **epoch milliseconds**. * `from` defaults to 30 days before `to`. * `to` defaults to the current time. * Maximum range: **366 days**. Longer ranges return a 400. ## Recipes [#recipes] ### Top 10 tools my org called last week [#top-10-tools-my-org-called-last-week] ```bash WEEK_AGO=$(( $(date +%s) - 604800 ))000 NOW=$(date +%s)000 curl -X POST https://backend.composio.dev/api/v3.1/org/usage/tool_calls \ -H "x-org-api-key: YOUR_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"from\": ${WEEK_AGO}, \"to\": ${NOW}, \"group_by\": \"tool_slug\", \"limit\": 10 }" ``` ### Tool call count per user for my project this month [#tool-call-count-per-user-for-my-project-this-month] ```bash MONTH_AGO=$(( $(date +%s) - 2592000 ))000 NOW=$(date +%s)000 curl -X POST https://backend.composio.dev/api/v3.1/project/usage/tool_calls \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"from\": ${MONTH_AGO}, \"to\": ${NOW}, \"group_by\": \"user_id\", \"limit\": 50 }" ``` ### Which toolkits is a specific user using? [#which-toolkits-is-a-specific-user-using] ```bash curl -X POST https://backend.composio.dev/api/v3.1/project/usage/tool_calls \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "group_by": "toolkit_slug", "filters": { "user_id": "user_abc123" } }' ``` ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/org/usage/summary` | [Org usage summary](/reference/api-reference/organization/postOrgUsageSummary) | | `POST` | `/api/v3.1/org/usage/{entity_type}` | [Org usage breakdown](/reference/api-reference/organization/postOrgUsageByEntityType) | --- # Organization Management (/reference/api-reference/organization-management) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Do not edit directly. */} Organization Management API endpoints ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `GET` | `/api/v3.1/org/list` | [List organizations](/reference/api-reference/organization-management/getOrgList) | --- # Projects (/reference/api-reference/projects) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/projects.mdx, not this file. */} Projects are Composio's multi-tenancy primitive. Every Composio account belongs to an **organization**. Inside an organization, **projects** are isolated environments that scope your API keys, connected accounts, auth configs, and webhook configurations. Resources in one project are not accessible from another. ```mermaid graph LR ORG["Organization (org_xxx)"] --- P1["Project: Production (proj_xxx)"] ORG --- P2["Project: Staging (proj_xxx)"] ORG --- TM["Team Members"] P1 --- A1["API Keys"] P1 --- A2["Connected Accounts"] P1 --- A3["Auth Configs"] P1 --- A4["Webhook Config"] P2 --- B1["..."] ``` Common reasons to use multiple projects: * **Separate environments**: keep production and staging isolated * **Separate products**: keep resources for different apps independent * **Client isolation**: give each client their own project with separate credentials and data ## Managing projects [#managing-projects] Manage projects from the [dashboard](https://dashboard.composio.dev/~/org/) or via the API using an **organization API key** (`x-org-api-key`). > Project management endpoints use the `x-org-api-key` header, not the regular `x-api-key`. Find your org API key in the dashboard under **Settings > Organization**. There is no limit on the number of projects per organization. Project names must be unique within the organization. Create a project with `should_create_api_key: true` to get an API key back in the response: ```bash curl -X POST https://backend.composio.dev/api/v3.1/org/owner/project/new \ -H "x-org-api-key: YOUR_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "my-staging-project", "should_create_api_key": true }' ``` ```json { "id": "proj_abc123xyz456", "name": "my-staging-project", "api_key": "ak_abc123xyz456" } ``` The list endpoint supports pagination with `limit` and `cursor`; getting a project by ID returns the full project object including its API keys. ## Project settings [#project-settings] Each project has settings that control security, logging, and display behavior. The project detail endpoints return current configuration for inspection. Use **Settings > Project Settings** in the [dashboard](https://dashboard.composio.dev/~/project/settings/general) to update project settings. Notable security setting: `require_mcp_api_key`, when `true`, requires MCP server requests to include a valid `x-api-key` header. This defaults to `true` for organizations created on or after March 5, 2026. ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/project/usage/summary` | [Project usage summary](/reference/api-reference/projects/postProjectUsageSummary) | | `POST` | `/api/v3.1/project/usage/{entity_type}` | [Project usage breakdown](/reference/api-reference/projects/postProjectUsageByEntityType) | | `GET` | `/api/v3.1/org/project/list` | [List all projects](/reference/api-reference/projects/getOrgProjectList) | | `POST` | `/api/v3.1/org/owner/project/new` | [Create a new project](/reference/api-reference/projects/postOrgOwnerProjectNew) | | `GET` | `/api/v3.1/org/owner/project/list` | [List all projects](/reference/api-reference/projects/getOrgOwnerProjectList) | | `GET` | `/api/v3.1/org/owner/project/{nano_id}` | [Get project details by ID With Org Api key](/reference/api-reference/projects/getOrgOwnerProjectByNanoId) | | `DELETE` | `/api/v3.1/org/owner/project/{nano_id}` | [Delete a project](/reference/api-reference/projects/deleteOrgOwnerProjectByNanoId) | | `POST` | `/api/v3.1/org/owner/project/{nano_id}/regenerate_api_key` | [Delete and generate new API key for project](/reference/api-reference/projects/postOrgOwnerProjectByNanoIdRegenerateApiKey) | --- # Sessions (prev Tool Router) (/reference/api-reference/tool-router) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/tool-router.mdx, not this file. */} These are Composio's session endpoints. A **session** is the runtime context your agent uses to work for one of your users: it scopes which user's connected accounts are in play, which tools are available, how authentication happens, and where execution state lives. Read [What is a session?](/docs/how-composio-works) for the full concept. > Sessions were formerly called the "tool router", which is why these endpoints live under `tool_router`. They are the same thing. In the SDK you do not call these endpoints directly. Use `composio.create(...)` to start a session and `composio.use(...)` to resume one, then call `session.tools()`, `session.execute(...)`, and `session.authorize(...)` on the returned object. Reach for the raw API when you need lower-level control: creating and patching a session config, attaching to an existing session, searching for tools, executing tools and meta tools, opening link sessions for auth, proxying authenticated requests, and reading or writing files in a session mount. See [Configuring sessions](/docs/configuring-sessions) for toolkits, auth configs, account selection, and presets. ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/tool_router/session` | [Create a new tool router session](/reference/api-reference/tool-router/postToolRouterSession) | | `GET` | `/api/v3.1/tool_router/session/{session_id}` | [Get a tool router session by ID (v3.1)](/reference/api-reference/tool-router/getToolRouterSessionBySessionId) | | `PATCH` | `/api/v3.1/tool_router/session/{session_id}` | [Patch a tool router session config (v3.1)](/reference/api-reference/tool-router/patchToolRouterSessionBySessionId) | | `DELETE` | `/api/v3.1/tool_router/session/{session_id}` | [Delete a tool router session](/reference/api-reference/tool-router/deleteToolRouterSessionBySessionId) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/attach` | [Attach to an existing tool router session (v3.1)](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdAttach) | | `GET` | `/api/v3.1/tool_router/session/{session_id}/config_history` | [List a tool router session config history](/reference/api-reference/tool-router/getToolRouterSessionBySessionIdConfigHistory) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/search` | [Search for tools using a query](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdSearch) | | `GET` | `/api/v3.1/tool_router/session/{session_id}/tools` | [List tools with schemas for a tool router session (v3.1)](/reference/api-reference/tool-router/getToolRouterSessionBySessionIdTools) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/execute` | [Execute a tool within a tool router session](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdExecute) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/execute_meta` | [Execute a meta tool within a tool router session](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdExecuteMeta) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/link` | [Create a link session for a toolkit in a tool router session](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdLink) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/proxy_execute` | [Execute proxy request within a tool router session](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdProxyExecute) | | `GET` | `/api/v3.1/tool_router/session/{session_id}/toolkits` | [Get toolkits for a tool router session](/reference/api-reference/tool-router/getToolRouterSessionBySessionIdToolkits) | | `GET` | `/api/v3.1/tool_router/session/{session_id}/mounts/{mount_id}/items` | [List files in a session mount](/reference/api-reference/tool-router/getToolRouterSessionBySessionIdMountsByMountIdItems) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/mounts/{mount_id}/download_url` | [Create a presigned download URL for a mount file](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdMountsByMountIdDownloadUrl) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/mounts/{mount_id}/upload_url` | [Create a presigned upload URL for a mount file](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdMountsByMountIdUploadUrl) | | `POST` | `/api/v3.1/tool_router/session/{session_id}/mounts/{mount_id}/delete` | [Delete a file from a session mount](/reference/api-reference/tool-router/postToolRouterSessionBySessionIdMountsByMountIdDelete) | --- # Toolkits (/reference/api-reference/toolkits) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/toolkits.mdx, not this file. */} A toolkit is a collection of related tools for a single app, like `gmail`, `github`, or `slack`. Each toolkit groups the actions for that service, its authentication requirements, and the trigger types it exposes. Reach for these endpoints when you want to: * List the toolkits in the catalog, sorted by popularity, to browse what is available before configuring a session. * Fetch a single toolkit by `slug` for its name, logo, categories, and metadata. * Fetch several toolkits at once with the multi endpoint. * List the available toolkit categories to filter the catalog by use case. * Read the toolkits changelog to track when tools or schemas change. These endpoints authenticate with your project API key in the `x-api-key` header. > Tools within a toolkit are versioned. When you execute a tool, resolve to a known version with `toolkit_versions=latest` or a pinned dated version. See the [toolkit versioning migration guide](/docs/migration-guide/toolkit-versioning). To browse toolkits visually, see the [toolkits catalog](/toolkits). For the concepts and SDK usage, see [Tools and toolkits](/docs/how-composio-works) and [Configuring sessions](/docs/configuring-sessions). ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `DELETE` | `/api/v3.1/custom/toolkits/{slug}` | [Delete a custom toolkit](/reference/api-reference/toolkits/deleteCustomToolkitsBySlug) | | `POST` | `/api/v3.1/toolkits/{toolkit_slug}/scopes/recommended` | [Get required scopes](/reference/api-reference/toolkits/recommendToolkitScopes) | | `GET` | `/api/v3.1/toolkits/{toolkit_slug}/scopes/grant_context` | [List grant_context options](/reference/api-reference/toolkits/recommendToolkitScopesGrantContext) | | `GET` | `/api/v3.1/toolkits` | [List available toolkits](/reference/api-reference/toolkits/getToolkits) | | `GET` | `/api/v3.1/toolkits/categories` | [List toolkit categories](/reference/api-reference/toolkits/getToolkitsCategories) | | `POST` | `/api/v3.1/custom/toolkits/upsert` | [Upsert a custom toolkit](/reference/api-reference/toolkits/postCustomToolkitsUpsert) | | `POST` | `/api/v3.1/custom/toolkits/sync` | [Sync a custom toolkit](/reference/api-reference/toolkits/postCustomToolkitsSync) | | `GET` | `/api/v3.1/toolkits/{slug}` | [Get toolkit by slug](/reference/api-reference/toolkits/getToolkitsBySlug) | | `POST` | `/api/v3.1/toolkits/multi` | [Fetch multiple toolkits](/reference/api-reference/toolkits/postToolkitsMulti) | | `GET` | `/api/v3.1/toolkits/changelog` | [Get toolkits changelog](/reference/api-reference/toolkits/getToolkitsChangelog) | --- # Tools (/reference/api-reference/tools) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/tools.mdx, not this file. */} Tools are the individual executable actions inside a toolkit, like `GMAIL_SEND_EMAIL` or `GITHUB_CREATE_ISSUE`. Each tool has an input schema describing its parameters and an output schema describing what it returns. Tool slugs are SCREAMING\_SNAKE\_CASE and follow a `{TOOLKIT}_{ACTION}` pattern. Reach for these endpoints when you want to: * List or search the catalog of available tools, optionally scoped to one or more toolkits. * Fetch a single tool's input and output schema by `tool_slug` before constructing a call. * Execute a tool on behalf of a user's connected account, or generate the inputs for an execution from natural language. * Look up the OAuth scopes a set of tools requires, or send an authenticated proxy request to an app's underlying API. These endpoints authenticate with your project API key in the `x-api-key` header. > Manual tool execution requires an explicit toolkit version. Pass `toolkit_versions=latest` (or pin a dated version like `20251027_00`) so calls resolve to a known tool definition. See the [toolkit versioning migration guide](/docs/migration-guide/toolkit-versioning). For the concepts behind tools, schemas, and authentication, see [Tools and toolkits](/docs/how-composio-works). ## Proxy execute [#proxy-execute] Proxy execute sends an authenticated HTTP request through a toolkit's [connected account](/docs/auth-configuration/connected-accounts) without a predefined tool, and Composio injects the OAuth token, API key, or other credentials on the server side so your code never touches raw secrets. Reach for it when you need an endpoint that Composio's predefined tools do not cover, when you need a request shape (custom query parameters, field masks, or advanced filters) that a predefined tool cannot express, or when a terminal agent would otherwise hardcode a bearer token in a `curl` call. Call it with `composio.tools.proxyExecute()` in the TypeScript SDK, `composio.tools.proxy()` in the Python SDK, or `POST /api/v3.1/tools/execute/proxy` over HTTP. The `endpoint` can be an absolute URL (`https://api.example.com/v1/resource`) or a path relative to the toolkit's base URL (`/v1/resource`), `method` is the HTTP verb, `connectedAccountId` selects the account to authenticate as, `body` carries the JSON payload, and `parameters` adds extra headers and query parameters. The response forwards the upstream `status`, `headers`, and parsed `data` verbatim. ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const { status, data } = await composio.tools.proxyExecute({ endpoint: '/repos/composiohq/composio/issues/1', method: 'GET', connectedAccountId: 'ca_github_user_123', parameters: [{ name: 'Accept', value: 'application/vnd.github.v3+json', in: 'header' }], }); console.log(status, data); ``` > Proxy execute rejects cross-domain requests, so the `endpoint` must resolve to the same domain as the connected account's toolkit, and you should not set the `Authorization` header yourself because Composio injects the correct credential for the account's auth scheme. This is an intentional security boundary, not a quota, so it cannot be bypassed by reshaping the request. Proxy execute is a form of [direct tool execution](/docs/sessions-vs-direct-execution): it bypasses session state, tool schemas, and modifiers. If you are building an agent, prefer [sessions](/docs/configuring-sessions), and use the proxy only for the specific API call that is not available as a tool. The full request and response schema lives in the [`POST /api/v3.1/tools/execute/proxy`](/reference/api-reference/tools/postToolsExecuteProxy) reference. ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `GET` | `/api/v3.1/tools` | [List available tools](/reference/api-reference/tools/getTools) | | `GET` | `/api/v3.1/tools/enum` | [Get tool enum list](/reference/api-reference/tools/getToolsEnum) | | `GET` | `/api/v3.1/tools/{tool_slug}` | [Get tool by slug](/reference/api-reference/tools/getToolsByToolSlug) | | `POST` | `/api/v3.1/tools/execute/{tool_slug}` | [Execute tool](/reference/api-reference/tools/postToolsExecuteByToolSlug) | | `POST` | `/api/v3.1/tools/execute/{tool_slug}/input` | [Generate tool inputs from natural language](/reference/api-reference/tools/postToolsExecuteByToolSlugInput) | | `POST` | `/api/v3.1/tools/execute/proxy` | [Execute proxy request](/reference/api-reference/tools/postToolsExecuteProxy) | | `POST` | `/api/v3.1/tools/scopes/required` | [Get required scopes for tools](/reference/api-reference/tools/postToolsScopesRequired) | --- # MCP (/reference/api-reference/mcp) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/mcp.mdx, not this file. */} > This API is deprecated. Use a session's MCP endpoint instead. Create a session with `composio.create(userId, { mcp: true })`, then read the hosted URL off `session.mcp.url`. See [Using sessions via MCP](/docs/sessions-via-mcp) and [migrating MCP servers to sessions](/docs/migration-guide/mcp-servers-to-sessions). The MCP API is the standalone, hosted [Model Context Protocol](https://modelcontextprotocol.io) server-management surface. It let you stand up and manage a separate server config per toolkit, then mint a per-user MCP URL that any MCP-compatible client could connect to. These endpoints create, list, update, and delete MCP servers, including custom servers spanning multiple apps, generate per-user MCP URLs, and manage per-user server instances and their connected accounts. Sessions replace this. A single `composio.create(...)` gives you the same MCP URL pattern, keyed by `user_id`, while handling tool discovery, authentication, context, and versioning for you. Your existing tools, auth configs (`ac_…`), and connected accounts carry over with no re-authentication. To pin a session to a fixed tool list the way a server did, use the direct-tools preset described in [Configuring sessions](/docs/configuring-sessions). ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `GET` | `/api/v3.1/mcp/servers` | [List MCP servers with optional filters and pagination (Legacy)](/reference/api-reference/mcp/getMcpServers) | | `POST` | `/api/v3.1/mcp/servers` | [Create a new MCP server (Legacy)](/reference/api-reference/mcp/postMcpServers) | | `POST` | `/api/v3.1/mcp/servers/custom` | [Create a new custom MCP server with multiple apps (Legacy)](/reference/api-reference/mcp/postMcpServersCustom) | | `POST` | `/api/v3.1/mcp/servers/generate` | [Generate MCP URL with custom parameters (Legacy)](/reference/api-reference/mcp/postMcpServersGenerate) | | `GET` | `/api/v3.1/mcp/{id}` | [Get MCP server details by ID (Legacy)](/reference/api-reference/mcp/getMcpById) | | `PATCH` | `/api/v3.1/mcp/{id}` | [Update MCP server configuration (Legacy)](/reference/api-reference/mcp/patchMcpById) | | `DELETE` | `/api/v3.1/mcp/{id}` | [Delete an MCP server (Legacy)](/reference/api-reference/mcp/deleteMcpById) | | `GET` | `/api/v3.1/mcp/app/{appKey}` | [List MCP servers for a specific app (Legacy)](/reference/api-reference/mcp/getMcpAppByAppKey) | | `GET` | `/api/v3.1/mcp/servers/{serverId}/instances` | [List all instances for an MCP server (Legacy)](/reference/api-reference/mcp/getMcpServersByServerIdInstances) | | `POST` | `/api/v3.1/mcp/servers/{serverId}/instances` | [Create a new MCP server instance (Legacy)](/reference/api-reference/mcp/postMcpServersByServerIdInstances) | | `DELETE` | `/api/v3.1/mcp/servers/{serverId}/instances/{instanceId}` | [Delete an MCP server instance and associated connected accounts (Legacy)](/reference/api-reference/mcp/deleteMcpServersByServerIdInstancesByInstanceId) | --- # Triggers (/reference/api-reference/triggers) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/triggers.mdx, not this file. */} Triggers let you subscribe to events from a user's connected app, such as a new Gmail message, a GitHub commit, or a Slack message, and receive the event data as a structured payload at your webhook endpoint. There are two layers to understand: * A **trigger type** is a template that defines what event to listen for and what configuration it needs. For example, `GITHUB_COMMIT_EVENT` requires an `owner` and a `repo`. Each toolkit exposes its own trigger types. * A **trigger instance** is a trigger type scoped to a specific user and connected account. Creating one produces an instance with its own `ti_*` ID that you can enable, disable, or delete independently. Reach for these endpoints when you want to: * Discover the trigger types a toolkit offers, or fetch one type by `slug` to inspect its config and payload schema. * Create or update a trigger instance for a connected account with the upsert endpoint. * List active trigger instances, or enable, disable, and delete an instance by `triggerId`. These endpoints authenticate with your project API key in the `x-api-key` header. > Creating a trigger instance only registers it. To actually receive events, set a webhook URL for your project once, then route incoming events on `metadata.trigger_slug`. See [Subscribing to events](/docs/setting-up-triggers/subscribing-to-events). For the full concept overview, see [Triggers](/docs/triggers). ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/trigger_instances/{slug}/upsert` | [Create or update a trigger](/reference/api-reference/triggers/postTriggerInstancesBySlugUpsert) | | `GET` | `/api/v3.1/trigger_instances/active` | [List active triggers](/reference/api-reference/triggers/getTriggerInstancesActive) | | `DELETE` | `/api/v3.1/trigger_instances/manage/{triggerId}` | [Delete a trigger](/reference/api-reference/triggers/deleteTriggerInstancesManageByTriggerId) | | `PATCH` | `/api/v3.1/trigger_instances/manage/{triggerId}` | [Enable or disable a trigger](/reference/api-reference/triggers/patchTriggerInstancesManageByTriggerId) | | `GET` | `/api/v3.1/triggers_types/list/enum` | [List trigger type enums](/reference/api-reference/triggers/getTriggersTypesListEnum) | | `GET` | `/api/v3.1/triggers_types/{slug}` | [Get trigger type by slug](/reference/api-reference/triggers/getTriggersTypesBySlug) | | `GET` | `/api/v3.1/triggers_types` | [List trigger types](/reference/api-reference/triggers/getTriggersTypes) | --- # Webhook Subscriptions (/reference/api-reference/webhook-subscriptions) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/webhook-subscriptions.mdx, not this file. */} Webhook subscriptions are outbound delivery configurations. They define the URL Composio posts [trigger](/docs/triggers) events to, along with the signing secret and the set of event types you want to receive. Reach for these endpoints to register where Composio should send events, to filter delivery to specific event types, and to manage the signing secret used to verify those deliveries. List the available event types with the `/webhook_subscriptions/event_types` endpoint, then create a subscription scoped to the ones you care about. Each subscription is addressed by its `id`. You can update its URL and filters with `PATCH`, delete it, and rotate its signing secret with `/webhook_subscriptions/{id}/rotate_secret` if the secret leaks. Every webhook request Composio sends includes `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. Store the secret as `COMPOSIO_WEBHOOK_SECRET` and verify each payload before trusting it. See [Verifying signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures) for the SDK and manual verification flows. ## Event types [#event-types] A subscription's `enabled_events` controls which events get delivered to its URL. Two broad families exist: * **Trigger events** like `composio.trigger.message` — payloads emitted by [triggers](/docs/triggers) you've enabled (new email, new issue, etc.). * **Lifecycle events** like `composio.connected_account.expired` — emitted when a [connected account](/docs/auth-configuration/connected-accounts) changes state. List everything you can subscribe to with the `/webhook_subscriptions/event_types` endpoint, then scope a subscription to the events you handle. ## Detecting connection expiry [#detecting-connection-expiry] Composio automatically refreshes OAuth tokens before they expire. But when a refresh token is revoked or expires, the connection enters an `EXPIRED` state and the user must re-authenticate. Subscribe to the `composio.connected_account.expired` event to detect this proactively, instead of waiting for a tool execution to fail. > This event is only available with [V3 webhook payloads](/docs/setting-up-triggers/subscribing-to-events#webhook-payload-versions). New organizations use V3 by default. Add `composio.connected_account.expired` to the subscription's `enabled_events`: ```bash curl -X POST https://backend.composio.dev/api/v3.1/webhook_subscriptions \ -H "X-API-KEY: " \ -H "Content-Type: application/json" \ -d '{ "webhook_url": "https://example.com/webhook", "enabled_events": [ "composio.trigger.message", "composio.connected_account.expired" ] }' ``` When a connection expires, Composio sends a webhook with the connected account details: ```json { "id": "evt_847cdfcd-d219-4f18-a6dd-91acd42ca94a", "type": "composio.connected_account.expired", "metadata": { "project_id": "pr_your-project-id", "org_id": "ok_your-org-id" }, "data": { "id": "ca_your-connected-account-id", "toolkit": { "slug": "gmail" }, "auth_config": { "id": "ac_your-auth-config-id", "auth_scheme": "OAUTH2" }, "status": "EXPIRED", "status_reason": "OAuth refresh token expired" }, "timestamp": "2026-02-06T12:00:00.000Z" } ``` Route on `type` to handle expiry alongside trigger events: **Python:** ```python from composio import Composio, WebhookEventType composio = Composio() @app.post("/webhook") async def webhook_handler(request: Request): payload = await request.json() event_type = payload.get("type") if event_type == WebhookEventType.CONNECTION_EXPIRED: account_id = payload["data"]["id"] toolkit = payload["data"]["toolkit"]["slug"] # Look up the user and send them a re-auth link session = composio.create(user_id=lookup_user(account_id)) connection_request = session.authorize(toolkit) notify_user(connection_request.redirect_url) elif event_type == WebhookEventType.TRIGGER_MESSAGE: # Handle trigger events pass return {"status": "ok"} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); type NextApiRequest = { body: any }; type NextApiResponse = { status: (code: number) => { json: (data: any) => void } }; declare function lookupUser(accountId: string): string; declare function notifyUser(url: string): void; export default async function webhookHandler(req: NextApiRequest, res: NextApiResponse) { const payload = req.body; if (payload.type === 'composio.connected_account.expired') { const accountId = payload.data.id; const toolkit = payload.data.toolkit.slug; // Look up the user and send them a re-auth link const session = await composio.create(lookupUser(accountId)); const connectionRequest = await session.authorize(toolkit); if (connectionRequest.redirectUrl) { notifyUser(connectionRequest.redirectUrl); } } else if (payload.type === 'composio.trigger.message') { // Handle trigger events } res.status(200).json({ status: 'ok' }); } ``` > Always [verify webhook signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures) before processing events in production. ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/webhook_subscriptions` | [Create webhook subscription](/reference/api-reference/webhook-subscriptions/postWebhookSubscriptions) | | `GET` | `/api/v3.1/webhook_subscriptions` | [List webhook subscriptions](/reference/api-reference/webhook-subscriptions/getWebhookSubscriptions) | | `GET` | `/api/v3.1/webhook_subscriptions/{id}` | [Get webhook subscription](/reference/api-reference/webhook-subscriptions/getWebhookSubscriptionsById) | | `PATCH` | `/api/v3.1/webhook_subscriptions/{id}` | [Update webhook subscription](/reference/api-reference/webhook-subscriptions/patchWebhookSubscriptionsById) | | `DELETE` | `/api/v3.1/webhook_subscriptions/{id}` | [Delete webhook subscription](/reference/api-reference/webhook-subscriptions/deleteWebhookSubscriptionsById) | | `POST` | `/api/v3.1/webhook_subscriptions/{id}/rotate_secret` | [Rotate webhook secret](/reference/api-reference/webhook-subscriptions/postWebhookSubscriptionsByIdRotateSecret) | | `GET` | `/api/v3.1/webhook_subscriptions/event_types` | [List available event types](/reference/api-reference/webhook-subscriptions/getWebhookSubscriptionsEventTypes) | --- # Webhook Events (/reference/api-reference/webhook-events) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from openapi-webhooks.json. Edit the overview at api-overviews/webhook-events.mdx, not this file. */} Webhook events delivered by the Composio platform to your registered endpoints. Configure your webhook subscriptions via the [Webhook Subscriptions API](/reference/api-reference/webhook-subscriptions/postWebhookSubscriptions), and verify signatures as described in [Verifying signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures). ## Events [#events] | Event | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------ | | `composio.trigger.message` | [Trigger message](/reference/api-reference/webhook-events/composio_trigger_message) | | `composio.connected_account.expired` | [Connection expired](/reference/api-reference/webhook-events/composio_connected_account_expired) | | `composio.trigger.disabled` | [Trigger disabled](/reference/api-reference/webhook-events/composio_trigger_disabled) | ## Legacy payloads (deprecated) [#legacy-payloads-deprecated] Older subscriptions may still receive these payload formats. The event type is unchanged — only the payload shape differs, selected by the subscription's version. You can upgrade an existing subscription at any time by updating its `version` — see [Update a webhook subscription](/reference/api-reference/webhook-subscriptions/patchWebhookSubscriptionsById). New integrations should use the current events above. | Event | Version | Description | | -------------------------- | ------- | ------------------------------------------------------------------------------------------- | | `composio.trigger.message` | V2 | [Trigger message (V2)](/reference/api-reference/webhook-events/composio_trigger_message_v2) | | `composio.trigger.message` | V1 | [Trigger message (V1)](/reference/api-reference/webhook-events/composio_trigger_message_v1) | --- # AuthConfigs (/reference/sdk-reference/python/auth-configs) ## Methods [#methods] ### list() [#list] Lists authentication configurations based on provided filter criteria. ```python def list(query: auth_config_list_params.AuthConfigListParams = ...) -> auth_config_list_response.AuthConfigListResponse ``` **Parameters** | Name | Type | | -------- | ---------------------------------------------- | | `query?` | `auth_config_list_params.AuthConfigListParams` | **Returns** `auth_config_list_response.AuthConfigListResponse` *** ### create() [#create] Create a new auth config ```python def create(toolkit: str, options: auth_config_create_params.AuthConfig) -> auth_config_create_response.AuthConfig ``` **Parameters** | Name | Type | | --------- | -------------------------------------- | | `toolkit` | `str` | | `options` | `auth_config_create_params.AuthConfig` | **Returns** `auth_config_create_response.AuthConfig` — The created auth config. *** ### get() [#get] Retrieves a specific authentication configuration by its ID ```python def get(nanoid: str) -> auth_config_retrieve_response.AuthConfigRetrieveResponse ``` **Parameters** | Name | Type | | -------- | ----- | | `nanoid` | `str` | **Returns** `auth_config_retrieve_response.AuthConfigRetrieveResponse` — The retrieved auth config. *** ### update() [#update] Updates an existing authentication configuration. This method allows you to modify properties of an auth config such as credentials, scopes, or tool restrictions. The update type (custom or default) determines which fields can be updated. ```python def update(nanoid: str, options: auth_config_update_params.AuthConfigUpdateParams) -> Dict ``` **Parameters** | Name | Type | | --------- | -------------------------------------------------- | | `nanoid` | `str` | | `options` | `auth_config_update_params.AuthConfigUpdateParams` | **Returns** `Dict` — The updated auth config. *** ### delete() [#delete] Deletes an existing authentication configuration. ```python def delete(nanoid: str) -> Dict ``` **Parameters** | Name | Type | | -------- | ----- | | `nanoid` | `str` | **Returns** `Dict` — The deleted auth config. *** ### enable() [#enable] Enables an existing authentication configuration. ```python def enable(nanoid: str) -> Dict ``` **Parameters** | Name | Type | | -------- | ----- | | `nanoid` | `str` | **Returns** `Dict` — The enabled auth config. *** ### disable() [#disable] Disables an existing authentication configuration. ```python def disable(nanoid: str) -> Dict ``` **Parameters** | Name | Type | | -------- | ----- | | `nanoid` | `str` | **Returns** `Dict` — The disabled auth config. *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/auth_configs.py#L18) --- # Composio (/reference/sdk-reference/python/composio) ## Properties [#properties] | Name | Type | | -------------------------------------------------------------------------- | ------------------- | | [`tools`](/reference/sdk-reference/python/tools) | `Tools` | | [`toolkits`](/reference/sdk-reference/python/toolkits) | `Toolkits` | | [`triggers`](/reference/sdk-reference/python/triggers) | `Triggers` | | [`auth_configs`](/reference/sdk-reference/python/auth-configs) | `AuthConfigs` | | [`connected_accounts`](/reference/sdk-reference/python/connected-accounts) | `ConnectedAccounts` | | [`mcp`](/reference/sdk-reference/python/mcp) | `MCP` | [View source](https://github.com/composiohq/composio/blob/next/python/composio/sdk.py#L49) --- # ConnectedAccounts (/reference/sdk-reference/python/connected-accounts) ## Methods [#methods] ### update() [#update] Update a connected account's alias and/or credentials. ```python def update(nanoid: str, alias: str | None = ..., connection: connected_account_patch_params.Connection | None = ...) -> connected_account_patch_response.ConnectedAccountPatchRes... ``` **Parameters** | Name | Type | | ------------- | --------------------------------------------------- | | `nanoid` | `str` | | `alias?` | `str \| None` | | `connection?` | `connected_account_patch_params.Connection \| None` | **Returns** `connected_account_patch_response.ConnectedAccountPatchRes...` — Response with `id`, `status`, and `success`. **Example** ```python # Set an alias composio.connected_accounts.update('ca_abc123', alias='work-gmail') # Clear an alias composio.connected_accounts.update('ca_abc123', alias='') ``` *** ### update\_acl() [#update_acl] Update the per-user ACL on a SHARED connected account. Experimental — shape may change in future releases. Only valid on SHARED connections; raises `ComposioAclOnlyForSharedError` on a PRIVATE connection. Omit a parameter to leave it unchanged; pass an empty list to clear an allow/deny list. At least one parameter must be provided. ```python def update_acl(nanoid: str, allow_all_users: bool | None = ..., allowed_user_ids: List[str | None] = ..., not_allowed_user_ids: List[str | None] = ...) -> connected_account_patch_response.ConnectedAccountPatchRes... ``` **Parameters** | Name | Type | | ----------------------- | ------------------- | | `nanoid` | `str` | | `allow_all_users?` | `bool \| None` | | `allowed_user_ids?` | `List[str \| None]` | | `not_allowed_user_ids?` | `List[str \| None]` | **Returns** `connected_account_patch_response.ConnectedAccountPatchRes...` — Response with `id`, `status`, and `success`. **Example** ```python composio.connected_accounts.update_acl( 'ca_abc', allow_all_users=True, not_allowed_user_ids=['user_bob'], ) ``` *** ### initiate() [#initiate] Compound function to create a new connected account. This function creates a new connected account and returns a connection request. Users can then wait for the connection to be established using the `wait_for_connection` method. ```python def initiate(user_id: str, auth_config_id: str, callback_url: str | None = ..., allow_multiple: bool = ..., config: connected_account_create_params.ConnectionState | None = ..., alias: str | None = ...) -> ConnectionRequest ``` **Parameters** | Name | Type | | ----------------- | --------------------------------------------------------- | | `user_id` | `str` | | `auth_config_id` | `str` | | `callback_url?` | `str \| None` | | `allow_multiple?` | `bool` | | `config?` | `connected_account_create_params.ConnectionState \| None` | | `alias?` | `str \| None` | **Returns** `ConnectionRequest` — The connection request. *** ### link() [#link] Create a Composio Connect Link for a user to connect their account to a given auth config. This method will return an external link which you can use for the user to connect their account. ```python def link(user_id: str, auth_config_id: str, callback_url: str | None = ..., alias: str | None = ..., allow_multiple: bool = ..., experimental: link_create_params.Experimental | None = ...) -> ConnectionRequest ``` **Parameters** | Name | Type | | ----------------- | ----------------------------------------- | | `user_id` | `str` | | `auth_config_id` | `str` | | `callback_url?` | `str \| None` | | `alias?` | `str \| None` | | `allow_multiple?` | `bool` | | `experimental?` | `link_create_params.Experimental \| None` | **Returns** `ConnectionRequest` — Connection request object. **Example** ```python # Create a connection request and redirect the user to the redirect url connection_request = composio.connected_accounts.link('user_123', 'auth_config_123') redirect_url = connection_request.redirect_url print(f"Visit: {redirect_url} to authenticate your account") # Wait for the connection to be established connected_account = connection_request.wait_for_connection() # Create a connection request with callback URL connection_request = composio.connected_accounts.link( 'user_123', 'auth_config_123', callback_url='https://your-app.com/callback' ) redirect_url = connection_request.redirect_url print(f"Visit: {redirect_url} to authenticate your account") # Wait for the connection to be established connected_account = composio.connected_accounts.wait_for_connection(connection_request.id) connection_request = composio.connected_accounts.link( 'user_creator', 'auth_config_123', experimental={ 'account_type': 'SHARED', 'acl_config_for_shared': { 'allow_all_users': True, 'not_allowed_user_ids': ['user_bob'], }, }, ) ``` *** ### wait\_for\_connection() [#wait_for_connection] Wait for connected account with given ID to be active ```python def wait_for_connection(id: str, timeout: float | None = ...) -> connected_account_retrieve_response.ConnectedAccountRetri... ``` **Parameters** | Name | Type | | ---------- | --------------- | | `id` | `str` | | `timeout?` | `float \| None` | **Returns** `connected_account_retrieve_response.ConnectedAccountRetri...` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/connected_accounts.py#L340) --- # Python SDK Reference (/reference/sdk-reference/python) # Python SDK Reference [#python-sdk-reference] Complete API reference for the `composio` Python package. ## Installation [#installation] ## Classes [#classes] | Class | Description | | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | [`Composio`](/reference/sdk-reference/python/composio) | Composio SDK for Python. Generic parameters: TTool: The individual tool type re... | | [`Tools`](/reference/sdk-reference/python/tools) | Tools class definition This class is used to manage tools in the Composio SDK. ... | | [`Toolkits`](/reference/sdk-reference/python/toolkits) | Toolkits are a collectiono of tools that can be used to perform various tasks. T... | | [`Triggers`](/reference/sdk-reference/python/triggers) | Triggers (instance) class | | [`ConnectedAccounts`](/reference/sdk-reference/python/connected-accounts) | Manage connected accounts. This class is used to manage connected accounts in t... | | [`AuthConfigs`](/reference/sdk-reference/python/auth-configs) | Manage authentication configurations. | | [`MCP`](/reference/sdk-reference/python/mcp) | MCP (Model Control Protocol) class. Provides enhanced MCP server operations Thi... | | [`Session`](/reference/sdk-reference/python/session) | A Composio session — the object returned by `composio.create(...)` / \`\`composi... | ## Quick Start [#quick-start] ```python from composio import Composio composio = Composio(api_key="your-api-key") # Get tools for a user tools = composio.tools.get("user-123", toolkits=["github"]) # Execute a tool result = composio.tools.execute( "GITHUB_GET_REPOS", arguments={"owner": "composio"}, user_id="user-123" ) ``` ## Decorators [#decorators] ### before\_execute [#before_execute] [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/_modifiers.py#L280) ```python @before_execute(modifier: BeforeExecute | None = ..., tools: List[str | None] = ..., toolkits: List[str | None] = ...) def my_modifier(...): ... ``` ### after\_execute [#after_execute] [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/_modifiers.py#L241) ```python @after_execute(modifier: AfterExecute | None = ..., tools: List[str | None] = ..., toolkits: List[str | None] = ...) def my_modifier(...): ... ``` ### before\_file\_upload [#before_file_upload] Build a `Modifier` for the file-upload hook (same scoping pattern as :func:`before_execute`). Your callable may take **either**: - a single `context` argument (:class:`BeforeFileUploadContext`) — the preferred form, exposes `context["source"]` (`"path"` or `"url"`), or - three positional arguments `(path, tool, toolkit)` — legacy form, kept for back-compat. Return a new path/URL string to substitute, or `False` to abort the upload (raises :class:`~composio.exceptions.FileUploadAbortedError`). Pass the returned `Modifier` in `modifiers=[...]` on :meth:`composio.core.models.tools.Tools.execute` or `tools.get`. Multiple such modifiers are composed in list order. [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/_modifiers.py#L319) ```python @before_file_upload(modifier: BeforeFileUploadLike | None = ..., tools: List[str | None] = ..., toolkits: List[str | None] = ...) def my_modifier(...): ... ``` ### schema\_modifier [#schema_modifier] [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/_modifiers.py#L377) ```python @schema_modifier(modifier: SchemaModifier | None = ..., tools: List[str | None] = ..., toolkits: List[str | None] = ...) def my_modifier(...): ... ``` --- # MCP (/reference/sdk-reference/python/mcp) > **Deprecated**: Use a session's MCP endpoint instead: `composio.create(user_id, mcp=True)` returns a session that exposes `session.mcp.url` / `session.mcp.headers`. MCP is now opt-in per session; this standalone `composio.mcp` server-management API is kept only for backwards compatibility. Prefer the session MCP endpoint; do not generate new code against `composio.mcp`. See [https://docs.composio.dev/docs/sessions-via-mcp](https://docs.composio.dev/docs/sessions-via-mcp) ## Methods [#methods] ### create() [#create] Create a new MCP server configuration with specified toolkits and authentication settings. ```python def create(name: str, toolkits: List[Union[ConfigToolkit, str]], manually_manage_connections: bool = ..., allowed_tools: List[str | None] = ...) -> MCPCreateResponse ``` **Parameters** | Name | Type | | ------------------------------ | --------------------------------- | | `name` | `str` | | `toolkits` | `List[Union[ConfigToolkit, str]]` | | `manually_manage_connections?` | `bool` | | `allowed_tools?` | `List[str \| None]` | **Returns** `MCPCreateResponse` — Created server details with generate method **Example** ```python >>> # Using toolkit configuration objects with auth >>> server = composio.experimental.mcp.create( ... 'personal-mcp-server', ... toolkits=[ ... { ... 'toolkit': 'github', ... 'auth_config_id': 'ac_xyz', ... }, ... { ... 'toolkit': 'slack', ... 'auth_config_id': 'ac_abc', ... }, ... ], ... allowed_tools=['GITHUB_CREATE_ISSUE', 'GITHUB_LIST_REPOS', 'SLACK_SEND_MESSAGE'], ... manually_manage_connections=False ... ) >>> >>> # Using simple toolkit names (most common usage) >>> server = composio.experimental.mcp.create( ... 'simple-mcp-server', ... toolkits=['composio_search', 'text_to_pdf'], ... allowed_tools=['COMPOSIO_SEARCH_DUCK_DUCK_GO_SEARCH', 'TEXT_TO_PDF_CONVERT_TEXT_TO_PDF'] ... ) >>> >>> # Using all tools from toolkits (default behavior) >>> server = composio.experimental.mcp.create( ... 'all-tools-server', ... toolkits=['composio_search', 'text_to_pdf'] ... # allowed_tools=None means all tools from these toolkits ... ) >>> >>> # Get server instance for a user >>> mcp = server.generate('user_12345') ``` *** ### list() [#list] List MCP servers with optional filtering and pagination. ```python def list(page_no: int | None = ..., limit: int | None = ..., toolkits: str | None = ..., auth_config_ids: str | None = ..., name: str | None = ..., order_by: Literal['created_at', 'updated_at' | None] = ..., order_direction: Literal['asc', 'desc' | None] = ...) -> MCPListResponse ``` **Parameters** | Name | Type | | ------------------ | --------------------------------------------- | | `page_no?` | `int \| None` | | `limit?` | `int \| None` | | `toolkits?` | `str \| None` | | `auth_config_ids?` | `str \| None` | | `name?` | `str \| None` | | `order_by?` | `Literal['created_at', 'updated_at' \| None]` | | `order_direction?` | `Literal['asc', 'desc' \| None]` | **Returns** `MCPListResponse` — Paginated list of MCP servers **Example** ```python >>> # List all servers >>> all_servers = composio.experimental.mcp.list() >>> >>> # List with pagination >>> paged_servers = composio.experimental.mcp.list(page_no=2, limit=5) >>> >>> # Filter by toolkit >>> github_servers = composio.experimental.mcp.list(toolkits='github', name='personal') ``` *** ### get() [#get] Retrieve detailed information about a specific MCP server/config. ```python def get(server_id: str) ``` **Parameters** | Name | Type | | ----------- | ----- | | `server_id` | `str` | **Example** ```python >>> server = composio.experimental.mcp.get('mcp_12345') >>> >>> print(server['name']) # "My Personal MCP Server" >>> print(server['allowed_tools']) # ["GITHUB_CREATE_ISSUE", "SLACK_SEND_MESSAGE"] >>> print(server['toolkits']) # ["github", "slack"] >>> print(server['server_instance_count']) # 3 ``` *** ### update() [#update] Update an existing MCP server configuration. ```python def update(server_id: str, name: str | None = ..., toolkits: List[Union[ConfigToolkit, str | None]] = ..., manually_manage_connections: bool | None = ..., allowed_tools: List[str | None] = ...) ``` **Parameters** | Name | Type | | ------------------------------ | ----------------------------------------- | | `server_id` | `str` | | `name?` | `str \| None` | | `toolkits?` | `List[Union[ConfigToolkit, str \| None]]` | | `manually_manage_connections?` | `bool \| None` | | `allowed_tools?` | `List[str \| None]` | **Example** ```python >>> # Update server name only >>> updated_server = composio.experimental.mcp.update( ... 'mcp_12345', ... name='My Updated MCP Server' ... ) >>> >>> # Update toolkits and tools >>> server_with_new_tools = composio.experimental.mcp.update( ... 'mcp_12345', ... toolkits=['github', 'slack'], ... allowed_tools=['GITHUB_CREATE_ISSUE', 'SLACK_SEND_MESSAGE'] ... ) >>> >>> # Update with auth configs >>> server_with_auth = composio.experimental.mcp.update( ... 'mcp_12345', ... toolkits=[ ... {'toolkit': 'github', 'auth_config_id': 'auth_abc123'}, ... {'toolkit': 'slack', 'auth_config_id': 'auth_def456'} ... ], ... allowed_tools=['GITHUB_CREATE_ISSUE', 'SLACK_SEND_MESSAGE'], ... manually_manage_connections=False ... ) ``` *** ### delete() [#delete] Permanently delete an MCP server configuration. ```python def delete(server_id: str) -> Dict[str, Any] ``` **Parameters** | Name | Type | | ----------- | ----- | | `server_id` | `str` | **Returns** `Dict[str, Any]` — Deletion result **Example** ```python >>> # Delete a server >>> result = composio.experimental.mcp.delete('mcp_12345') >>> >>> if result['deleted']: ... print(f"Server {result['id']} has been successfully deleted") >>> else: ... print(f"Failed to delete server {result['id']}") ``` *** ### generate() [#generate] Get server URLs for an existing MCP server. This matches the TypeScript implementation exactly. ```python def generate(user_id: str, mcp_config_id: str, manually_manage_connections: bool | None = ...) -> MCPServerInstance ``` **Parameters** | Name | Type | | ------------------------------ | -------------- | | `user_id` | `str` | | `mcp_config_id` | `str` | | `manually_manage_connections?` | `bool \| None` | **Returns** `MCPServerInstance` — MCP server instance **Example** ```python >>> mcp = composio.experimental.mcp.generate( ... 'user_12345', ... 'mcp_67890', ... manually_manage_connections=False ... ) >>> >>> print(mcp['url']) # Server URL for the user >>> print(mcp['allowed_tools']) # Available tools ``` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/mcp.py#L104) --- # Session (/reference/sdk-reference/python/session) ## Properties [#properties] | Name | Type | | -------------- | --------------------------------- | | `session_id` | `str` | | `experimental` | `'ToolRouterSessionExperimental'` | | `preload` | `Any` | ## Methods [#methods] ### tools() [#tools] Get provider-wrapped tools for execution with your AI framework. Returns tools configured for this session, wrapped in the format expected by your AI provider (OpenAI, Anthropic, LangChain, etc.). When custom tools are bound to the session, execution of COMPOSIO\_MULTI\_EXECUTE\_TOOL is intercepted: local tools are executed in-process, remote tools are sent to the backend. ```python def tools(modifiers: 'Modifiers' | None = ...) -> TToolCollection ``` **Parameters** | Name | Type | | ------------ | --------------------- | | `modifiers?` | `'Modifiers' \| None` | **Returns** `TToolCollection` *** ### authorize() [#authorize] Authorize a toolkit for the user and get a connection request. Initiates the OAuth flow and returns a ConnectionRequest with redirect URL. ```python def authorize(toolkit: str, callback_url: str | None = ..., alias: str | None = ..., experimental: session_link_params.Experimental | None = ...) -> ConnectionRequest ``` **Parameters** | Name | Type | | --------------- | ------------------------------------------ | | `toolkit` | `str` | | `callback_url?` | `str \| None` | | `alias?` | `str \| None` | | `experimental?` | `session_link_params.Experimental \| None` | **Returns** `ConnectionRequest` *** ### toolkits() [#toolkits] Get toolkit connection states for the session. ```python def toolkits(toolkits: List[str | None] = ..., next_cursor: str | None = ..., limit: int | None = ..., is_connected: bool | None = ..., search: str | None = ...) -> ToolkitConnectionsDetails ``` **Parameters** | Name | Type | | --------------- | ------------------- | | `toolkits?` | `List[str \| None]` | | `next_cursor?` | `str \| None` | | `limit?` | `int \| None` | | `is_connected?` | `bool \| None` | | `search?` | `str \| None` | **Returns** `ToolkitConnectionsDetails` *** ### search() [#search] Search for tools by semantic use case. Returns relevant tools for the given query with schemas and guidance. ```python def search(query: str, model: str | None = ...) -> SessionSearchResponse ``` **Parameters** | Name | Type | | -------- | ------------- | | `query` | `str` | | `model?` | `str \| None` | **Returns** `SessionSearchResponse` *** ### execute() [#execute] Execute a tool within the session. For custom tools, accepts the full slug (e.g. "LOCAL\_GREP") or the original slug (e.g. "GREP") when that original slug is unique across the session's custom tools and toolkits. Custom tools are executed in-process; remote tools are sent to the Composio backend. ```python def execute(tool_slug: str, arguments: Dict[str, Any | None] = ..., account: str | None = ...) -> SessionExecuteResponse ``` **Parameters** | Name | Type | | ------------ | ------------------------ | | `tool_slug` | `str` | | `arguments?` | `Dict[str, Any \| None]` | | `account?` | `str \| None` | **Returns** `SessionExecuteResponse` *** ### custom\_tools() [#custom_tools] List all custom tools registered in this session. Returns tools with their final slugs, schemas, and resolved toolkit. ```python def custom_tools(toolkit: str | None = ...) -> List[RegisteredCustomTool] ``` **Parameters** | Name | Type | | ---------- | ------------- | | `toolkit?` | `str \| None` | **Returns** `List[RegisteredCustomTool]` — Array of registered custom tools *** ### custom\_toolkits() [#custom_toolkits] List all custom toolkits registered in this session. Returns toolkits with their tools showing final slugs. ```python def custom_toolkits() -> List[RegisteredCustomToolkit] ``` **Returns** `List[RegisteredCustomToolkit]` *** ### proxy\_execute() [#proxy_execute] Proxy an API call through Composio's auth layer. ```python def proxy_execute(toolkit: str, endpoint: str, method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], body: Any = ..., parameters: List[Dict[str, Any | None]] = ...) -> ToolRouterSessionProxyExecuteResponse ``` **Parameters** | Name | Type | | ------------- | -------------------------------------------------- | | `toolkit` | `str` | | `endpoint` | `str` | | `method` | `Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH']` | | `body?` | `Any` | | `parameters?` | `List[Dict[str, Any \| None]]` | **Returns** `ToolRouterSessionProxyExecuteResponse` — Proxied API response *** ### update() [#update] Partially update the session configuration. Only the fields provided will be changed; omitted fields are preserved. Mutates this session's `preload` in-place. Pass `None` for `manage_connections`, `sandbox`/`workbench`, or `multi_account` to clear the stored value. `workbench` is a backwards-compatible alias for `sandbox`. Prefer `sandbox` in new code. All parameters use the same types as the Stainless-generated `client.tool_router.session.patch()` method. ```python def update(toolkits: Union[session_patch_params.Toolkits, 'Omit'] = ..., tools: Union[Dict[str, session_patch_params.Tools], 'Omit'] = ..., tags: Union[session_patch_params.Tags, 'Omit'] = ..., auth_configs: Union[Dict[str, str], 'Omit'] = ..., connected_accounts: Union[Dict[str, SequenceNotStr[str | None]], 'Omit'] = ..., manage_connections: Union[session_patch_params.ManageConnections | None, 'Omit'] = ..., sandbox: Union[session_patch_params.Workbench | None, 'Omit'] = ..., workbench: Union[session_patch_params.Workbench | None, 'Omit'] = ..., multi_account: Union[session_patch_params.MultiAccount | None, 'Omit'] = ..., preload: Union[session_patch_params.Preload, 'Omit'] = ...) -> None ``` **Parameters** | Name | Type | | --------------------- | --------------------------------------------------------------- | | `toolkits?` | `Union[session_patch_params.Toolkits, 'Omit']` | | `tools?` | `Union[Dict[str, session_patch_params.Tools], 'Omit']` | | `tags?` | `Union[session_patch_params.Tags, 'Omit']` | | `auth_configs?` | `Union[Dict[str, str], 'Omit']` | | `connected_accounts?` | `Union[Dict[str, SequenceNotStr[str \| None]], 'Omit']` | | `manage_connections?` | `Union[session_patch_params.ManageConnections \| None, 'Omit']` | | `sandbox?` | `Union[session_patch_params.Workbench \| None, 'Omit']` | | `workbench?` | `Union[session_patch_params.Workbench \| None, 'Omit']` | | `multi_account?` | `Union[session_patch_params.MultiAccount \| None, 'Omit']` | | `preload?` | `Union[session_patch_params.Preload, 'Omit']` | *** ### delete() [#delete] Delete this session. Deleted sessions immediately stop being retrievable or executable. An already-deleted session surfaces the backend 404. ```python def delete() -> ToolRouterSessionDeleteResponse ``` **Returns** `ToolRouterSessionDeleteResponse` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/tool_router_session.py#L90) --- # Toolkits (/reference/sdk-reference/python/toolkits) ## Methods [#methods] ### list() [#list] List all toolkits. ```python def list(category: str | None = ..., cursor: str | None = ..., limit: float | None = ..., sort_by: Literal['usage', 'alphabetically' | None] = ..., managed_by: Literal['composio', 'all', 'project' | None] = ...) -> toolkit_list_response.ToolkitListResponse ``` **Parameters** | Name | Type | | ------------- | ----------------------------------------------- | | `category?` | `str \| None` | | `cursor?` | `str \| None` | | `limit?` | `float \| None` | | `sort_by?` | `Literal['usage', 'alphabetically' \| None]` | | `managed_by?` | `Literal['composio', 'all', 'project' \| None]` | **Returns** `toolkit_list_response.ToolkitListResponse` *** ### get() [#get] ```python def get(slug: str | None = ..., query: toolkit_list_params.ToolkitListParams | None = ...) -> Union[toolkit_retrieve_response.ToolkitRetrieveResponse, ... ``` **Parameters** | Name | Type | | -------- | ----------------------------------------------- | | `slug?` | `str \| None` | | `query?` | `toolkit_list_params.ToolkitListParams \| None` | **Returns** `Union[toolkit_retrieve_response.ToolkitRetrieveResponse, ...` *** ### list\_categories() [#list_categories] List all categories of toolkits. ```python def list_categories() ``` *** ### authorize() [#authorize] Authorize a user to a toolkit If auth config is not found, it will be created using composio managed auth. ```python def authorize(user_id: str, toolkit: str) ``` **Parameters** | Name | Type | | --------- | ----- | | `user_id` | `str` | | `toolkit` | `str` | *** ### get\_connected\_account\_initiation\_fields() [#get_connected_account_initiation_fields] Get the required property for a given toolkit and auth scheme. ```python def get_connected_account_initiation_fields(toolkit: str, auth_scheme: AuthSchemeL, required_only: bool = ...) -> AuthFieldsT ``` **Parameters** | Name | Type | | ---------------- | ------------- | | `toolkit` | `str` | | `auth_scheme` | `AuthSchemeL` | | `required_only?` | `bool` | **Returns** `AuthFieldsT` *** ### get\_auth\_config\_creation\_fields() [#get_auth_config_creation_fields] Get the required property for a given toolkit and auth scheme. ```python def get_auth_config_creation_fields(toolkit: str, auth_scheme: AuthSchemeL, required_only: bool = ...) -> AuthFieldsT ``` **Parameters** | Name | Type | | ---------------- | ------------- | | `toolkit` | `str` | | `auth_scheme` | `AuthSchemeL` | | `required_only?` | `bool` | **Returns** `AuthFieldsT` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/toolkits.py#L26) --- # Tools (/reference/sdk-reference/python/tools) ## Methods [#methods] ### get\_raw\_composio\_tool\_by\_slug() [#get_raw_composio_tool_by_slug] Returns schema for the given tool slug. ```python def get_raw_composio_tool_by_slug(slug: str) -> Tool ``` **Parameters** | Name | Type | | ------ | ----- | | `slug` | `str` | **Returns** `Tool` *** ### get\_raw\_composio\_tools() [#get_raw_composio_tools] Get a list of tool schemas based on the provided filters. ```python def get_raw_composio_tools(tools: list[str | None] = ..., search: str | None = ..., toolkits: list[str | None] = ..., scopes: List[str | None] = ..., limit: int | None = ...) -> list[Tool] ``` **Parameters** | Name | Type | | ----------- | ------------------- | | `tools?` | `list[str \| None]` | | `search?` | `str \| None` | | `toolkits?` | `list[str \| None]` | | `scopes?` | `List[str \| None]` | | `limit?` | `int \| None` | **Returns** `list[Tool]` *** ### get\_raw\_tool\_router\_meta\_tools() [#get_raw_tool_router_meta_tools] Fetches the tools exposed by a tool router session. This method fetches helper/meta tools and any preloaded app tools from the Composio API and transforms them to the expected format. It provides access to the underlying tool data without provider-specific wrapping. ```python def get_raw_tool_router_meta_tools(session_id: str, modifiers: 'Modifiers' | None = ...) -> list[Tool] ``` **Parameters** | Name | Type | | ------------ | --------------------- | | `session_id` | `str` | | `modifiers?` | `'Modifiers' \| None` | **Returns** `list[Tool]` — The list of meta tools **Example** ```python from composio import Composio composio = Composio() tools_model = composio.tools # Get meta tools for a session meta_tools = tools_model.get_raw_tool_router_meta_tools("session_123") print(meta_tools) # Get meta tools with schema modifiers from composio.core.models import schema_modifier @schema_modifier def modify_schema(tool: str, toolkit: str, schema): # Customize the schema schema.description = f"Modified: {schema.description}" return schema meta_tools = tools_model.get_raw_tool_router_meta_tools( "session_123", modifiers=[modify_schema] ) ``` *** ### get() [#get] Get a tool or list of tools based on the provided arguments. The return type is automatically inferred based on the provider's generic parameters. For example: - OpenAIProvider -> list\[ChatCompletionToolParam] - AnthropicProvider -> list\[ToolParam] - CustomProvider\[MyTool, list\[MyTool]] -> list\[MyTool] ```python def get(user_id: str, slug: str | None = ..., tools: list[str | None] = ..., search: str | None = ..., toolkits: list[str | None] = ..., scopes: List[str | None] = ..., modifiers: Modifiers | None = ..., limit: int | None = ...) -> TToolCollection ``` **Parameters** | Name | Type | | ------------ | ------------------- | | `user_id` | `str` | | `slug?` | `str \| None` | | `tools?` | `list[str \| None]` | | `search?` | `str \| None` | | `toolkits?` | `list[str \| None]` | | `scopes?` | `List[str \| None]` | | `modifiers?` | `Modifiers \| None` | | `limit?` | `int \| None` | **Returns** `TToolCollection` — Provider-specific tool collection (TToolCollection). *** ### execute() [#execute] Execute a tool with the provided parameters. This method calls the Composio API to execute the tool and returns the response. ```python def execute(slug: str, arguments: Dict, connected_account_id: str | None = ..., custom_auth_params: tool_execute_params.CustomAuthParams | None = ..., custom_connection_data: tool_execute_params.CustomConnectionData | None = ..., user_id: str | None = ..., text: str | None = ..., version: str | None = ..., dangerously_skip_version_check: bool | None = ..., modifiers: Modifiers | None = ...) -> ToolExecutionResponse ``` **Parameters** | Name | Type | | --------------------------------- | -------------------------------------------------- | | `slug` | `str` | | `arguments` | `Dict` | | `connected_account_id?` | `str \| None` | | `custom_auth_params?` | `tool_execute_params.CustomAuthParams \| None` | | `custom_connection_data?` | `tool_execute_params.CustomConnectionData \| None` | | `user_id?` | `str \| None` | | `text?` | `str \| None` | | `version?` | `str \| None` | | `dangerously_skip_version_check?` | `bool \| None` | | `modifiers?` | `Modifiers \| None` | **Returns** `ToolExecutionResponse` — The response from the tool. *** ### proxy() [#proxy] Proxy a tool call to the Composio API ```python def proxy(endpoint: str, method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], body: object | None = ..., connected_account_id: str | None = ..., parameters: List[tool_proxy_params.Parameter | None] = ..., custom_connection_data: tool_proxy_params.CustomConnectionData | None = ...) -> tool_proxy_response.ToolProxyResponse ``` **Parameters** | Name | Type | | ------------------------- | ---------------------------------------------------------- | | `endpoint` | `str` | | `method` | `Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']` | | `body?` | `object \| None` | | `connected_account_id?` | `str \| None` | | `parameters?` | `List[tool_proxy_params.Parameter \| None]` | | `custom_connection_data?` | `tool_proxy_params.CustomConnectionData \| None` | **Returns** `tool_proxy_response.ToolProxyResponse` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/tools.py#L119) --- # Triggers (/reference/sdk-reference/python/triggers) ## Methods [#methods] ### set\_webhook\_subscription() [#set_webhook_subscription] Create or update the project webhook subscription used for webhook delivery. If a subscription already exists, the first subscription is updated. Otherwise a new subscription is created. By default this subscribes to V3 trigger message events. ```python def set_webhook_subscription(webhook_url: str, enabled_events: Sequence[str | None] = ..., version: Union[WebhookVersion, str] = ...) -> WebhookSubscription ``` **Parameters** | Name | Type | | ----------------- | ---------------------------- | | `webhook_url` | `str` | | `enabled_events?` | `Sequence[str \| None]` | | `version?` | `Union[WebhookVersion, str]` | **Returns** `WebhookSubscription` **Example** ```python composio.triggers.set_webhook_subscription( webhook_url=f"{APP_URL}/webhooks/composio", ) ``` *** ### get\_type() [#get_type] Get a trigger type by its slug Uses the global toolkit version provided when initializing composio instance to fetch trigger for specific toolkit version ```python def get_type(slug: str) -> TriggersTypeRetrieveResponse ``` **Parameters** | Name | Type | | ------ | ----- | | `slug` | `str` | **Returns** `TriggersTypeRetrieveResponse` — The trigger type *** ### list\_active() [#list_active] List all active triggers ```python def list_active(trigger_ids: list[str | None] = ..., trigger_names: list[str | None] = ..., auth_config_ids: list[str | None] = ..., connected_account_ids: list[str | None] = ..., show_disabled: bool | None = ..., limit: int | None = ..., cursor: str | None = ...) ``` **Parameters** | Name | Type | | ------------------------ | ------------------- | | `trigger_ids?` | `list[str \| None]` | | `trigger_names?` | `list[str \| None]` | | `auth_config_ids?` | `list[str \| None]` | | `connected_account_ids?` | `list[str \| None]` | | `show_disabled?` | `bool \| None` | | `limit?` | `int \| None` | | `cursor?` | `str \| None` | *** ### list() [#list] List all the trigger types. ```python def list(cursor: str | None = ..., limit: int | None = ..., toolkit_slugs: list[str | None] = ...) ``` **Parameters** | Name | Type | | ---------------- | ------------------- | | `cursor?` | `str \| None` | | `limit?` | `int \| None` | | `toolkit_slugs?` | `list[str \| None]` | *** ### create() [#create] Create a trigger instance ```python def create(slug: str, user_id: str | None = ..., connected_account_id: str | None = ..., trigger_config: Dict[str, Any | None] = ...) -> trigger_instance_upsert_response.TriggerInstanceUpsertRes... ``` **Parameters** | Name | Type | | ----------------------- | ------------------------ | | `slug` | `str` | | `user_id?` | `str \| None` | | `connected_account_id?` | `str \| None` | | `trigger_config?` | `Dict[str, Any \| None]` | **Returns** `trigger_instance_upsert_response.TriggerInstanceUpsertRes...` — The trigger instance *** ### subscribe() [#subscribe] Subscribe to a trigger and receive trigger events. ```python def subscribe(timeout: float = ...) -> TriggerSubscription ``` **Parameters** | Name | Type | | ---------- | ------- | | `timeout?` | `float` | **Returns** `TriggerSubscription` — The trigger subscription handler. *** ### verify\_webhook() [#verify_webhook] Verify an incoming webhook payload and signature. This method validates that the webhook request is authentic by: 1. Validating the webhook timestamp is within the tolerance window 2. Verifying the HMAC-SHA256 signature using the correct algorithm 3. Parsing the payload and detecting the webhook version (V1, V2, or V3) ```python def verify_webhook(id: str, payload: str, secret: str, signature: str, timestamp: str, tolerance: int = ...) -> VerifyWebhookResult ``` **Parameters** | Name | Type | | ------------ | ----- | | `id` | `str` | | `payload` | `str` | | `secret` | `str` | | `signature` | `str` | | `timestamp` | `str` | | `tolerance?` | `int` | **Returns** `VerifyWebhookResult` — VerifyWebhookResult containing version, normalized payload, and raw payload :raises WebhookSignatureVerificationError: If the signature verification fails :raises WebhookPayloadError: If the payload cannot be parsed or is invalid **Example** ```python # In a Flask webhook handler @app.route('/webhook', methods=['POST']) def webhook(): try: result = composio.triggers.verify_webhook( id=request.headers.get('webhook-id', ''), payload=request.get_data(as_text=True), signature=request.headers.get('webhook-signature', ''), timestamp=request.headers.get('webhook-timestamp', ''), secret=os.environ['COMPOSIO_WEBHOOK_SECRET'], ) # Process the verified payload print(f"Version: {result['version']}") print(f"Received trigger: {result['payload']['trigger_slug']}") return 'OK', 200 except WebhookSignatureVerificationError: return 'Unauthorized', 401 ``` *** ### parse() [#parse] Parse an incoming webhook request into a typed, normalized trigger payload. Pass a framework request object, or pass `body=` and `headers=` explicitly. When `verify_secret` is provided, the SDK verifies the webhook signature before returning the normalized trigger payload. When it is omitted, the SDK parses the body without verifying the signature. `request` may be any object exposing the request body and headers, such as a Flask, Django, or FastAPI request. The body is read from `.body` (or `.data` / `.get_data()`), and the headers are read from `.headers`. Because this SDK is synchronous, async frameworks must pass an already-read raw body, for example via `body=await request.body()`. ```python def parse(request: Any = ..., body: Union[str, bytes, Mapping[str, Any], None] = ..., headers: Union[Mapping[str, Any], None] = ..., verify_secret: Union[str, None, Omit] = ..., tolerance: int = ...) -> VerifyWebhookResult ``` **Parameters** | Name | Type | | ---------------- | -------------------------------------------- | | `request?` | `Any` | | `body?` | `Union[str, bytes, Mapping[str, Any], None]` | | `headers?` | `Union[Mapping[str, Any], None]` | | `verify_secret?` | `Union[str, None, Omit]` | | `tolerance?` | `int` | **Returns** `VerifyWebhookResult` — VerifyWebhookResult containing version, normalized payload, and raw payload :raises ValidationError: If `verify_secret` is empty, or is set but signature headers are missing :raises WebhookSignatureVerificationError: If signature verification fails :raises WebhookPayloadError: If the payload cannot be parsed **Example** ```python # Flask: verify the signature @app.route('/webhooks/composio', methods=['POST']) def webhook(): try: result = composio.triggers.parse( request, verify_secret=os.environ['COMPOSIO_WEBHOOK_SECRET'], ) print(f"Trigger: {result['payload']['trigger_slug']}") print(f"Event data: {result['payload']['payload']}") return 'OK', 200 except exceptions.WebhookSignatureVerificationError: return 'Unauthorized', 401 # FastAPI: parse without verifying after reading the async body @app.post('/webhooks/composio') async def webhook(request: Request): raw = await request.body() result = composio.triggers.parse(body=raw, headers=request.headers) return {'trigger': result['payload']['trigger_slug']} ``` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/triggers.py#L936) --- # Webhook Endpoints (/reference/api-reference/webhook-endpoints) > **API version:** This page documents Composio REST API v3.1, the current version, at `https://backend.composio.dev/api/v3.1`. `https://backend.composio.dev/api/v3` is the previous version and remains supported. {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/webhook-endpoints.mdx, not this file. */} Webhook endpoints are per-OAuth-app webhook ingress configurations. They define the inbound URL a provider posts events to, along with the signing secret Composio stores and uses to verify those incoming payloads. Reach for these endpoints when an OAuth app you have configured needs to deliver provider-side events into Composio. You create an endpoint, configure or update it by its `nano_id`, and store the signing secret Composio uses to authenticate inbound requests. Each endpoint is addressed by its `nano_id`. The `POST` to `/webhook_endpoints/{nano_id}` replaces the full configuration, while `PATCH` updates it in place. This is distinct from [webhook subscriptions](/reference/api-reference/webhook-subscriptions), which control where Composio delivers outbound trigger events. To verify the signature on payloads Composio sends you, see [Verifying signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures). To set up the trigger events those payloads carry, see [Triggers](/docs/triggers). ## Endpoints [#endpoints] | Method | Path | Endpoint | | --- | --- | --- | | `POST` | `/api/v3.1/webhook_endpoints` | [Create webhook endpoint](/reference/api-reference/webhook-endpoints/postWebhookEndpoints) | | `GET` | `/api/v3.1/webhook_endpoints` | [List webhook endpoints](/reference/api-reference/webhook-endpoints/getWebhookEndpoints) | | `GET` | `/api/v3.1/webhook_endpoints/{nano_id}` | [Get webhook endpoint](/reference/api-reference/webhook-endpoints/getWebhookEndpointsByNanoId) | | `POST` | `/api/v3.1/webhook_endpoints/{nano_id}` | [Put webhook endpoint configuration](/reference/api-reference/webhook-endpoints/postWebhookEndpointsByNanoId) | | `PATCH` | `/api/v3.1/webhook_endpoints/{nano_id}` | [Update webhook endpoint configuration](/reference/api-reference/webhook-endpoints/patchWebhookEndpointsByNanoId) | --- # AuthConfigs (/reference/sdk-reference/typescript/auth-configs) ## Usage [#usage] Access this class through the `composio.authConfigs` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.authConfigs.list(); ``` ## Methods [#methods] ### create() [#create] Create a new auth config ```typescript async create(toolkit: string, options: CreateAuthConfigParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------- | | `toolkit` | `string` | Unique identifier of the toolkit | | `options` | `CreateAuthConfigParams` | Options for creating a new auth config | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Created auth config **Example** ```typescript const authConfig = await authConfigs.create('my-toolkit', { type: AuthConfigTypes.CUSTOM, name: 'My Custom Auth Config', authScheme: AuthSchemeTypes.API_KEY, credentials: { apiKey: '1234567890', }, }); ``` *** ### delete() [#delete] Deletes an authentication configuration. This method permanently removes an auth config from the Composio platform. This action cannot be undone and will prevent any connected accounts that use this auth config from functioning. ```typescript async delete(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to delete | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The deletion response **Example** ```typescript // Delete an auth config await composio.authConfigs.delete('auth_abc123'); ``` *** ### disable() [#disable] Disables an authentication configuration. This is a convenience method that calls updateStatus with 'DISABLED'. When disabled, the auth config cannot be used to create new connected accounts or authenticate with third-party services, but existing connections may continue to work. ```typescript async disable(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to disable | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated auth config details **Example** ```typescript // Disable an auth config await composio.authConfigs.disable('auth_abc123'); ``` *** ### enable() [#enable] Enables an authentication configuration. This is a convenience method that calls updateStatus with 'ENABLED'. When enabled, the auth config can be used to create new connected accounts and authenticate with third-party services. ```typescript async enable(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to enable | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated auth config details **Example** ```typescript // Enable an auth config await composio.authConfigs.enable('auth_abc123'); ``` *** ### get() [#get] Retrieves a specific authentication configuration by its ID. This method fetches detailed information about a single auth config and transforms the response to the SDK's standardized format. ```typescript async get(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ---------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to retrieve | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The auth config details **Example** ```typescript // Get an auth config by ID const authConfig = await composio.authConfigs.get('auth_abc123'); console.log(authConfig.name); // e.g., 'GitHub Auth' console.log(authConfig.toolkit.slug); // e.g., 'github' ``` *** ### list() [#list] Lists authentication configurations based on provided filter criteria. This method retrieves auth configs from the Composio API, transforms them to the SDK format, and supports filtering by various parameters. ```typescript async list(query?: AuthConfigListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ---------------------------------------------------- | | `query?` | `AuthConfigListParams` | Optional query parameters for filtering auth configs | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — A paginated list of auth configurations **Example** ```typescript // List all auth configs const allConfigs = await composio.authConfigs.list(); // List auth configs for a specific toolkit const githubConfigs = await composio.authConfigs.list({ toolkit: 'github' }); // Search auth configs by name or id const searchedConfigs = await composio.authConfigs.list({ search: 'github', showDisabled: true }); // List Composio-managed auth configs const managedConfigs = await composio.authConfigs.list({ isComposioManaged: true }); ``` *** ### update() [#update] Updates an existing authentication configuration. This method allows you to modify properties of an auth config such as credentials, scopes, or tool restrictions. The update type (custom or default) determines which fields can be updated. ```typescript async update(nanoid: string, data: AuthConfigUpdateParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to update | | `data` | `AuthConfigUpdateParams` | The data to update, which can be either custom or default type | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated auth config **Example** ```typescript // Update a custom auth config with new credentials const updatedConfig = await composio.authConfigs.update('auth_abc123', { type: 'custom', credentials: { apiKey: 'new-api-key-value' } }); // Update a default auth config with new scopes const updatedConfig = await composio.authConfigs.update('auth_abc123', { type: 'default', scopes: ['read:user', 'repo'] }); ``` *** ### updateStatus() [#updatestatus] Updates the status of an authentication configuration. This method allows you to enable or disable an auth config. When disabled, the auth config cannot be used to create new connected accounts or authenticate with third-party services. ```typescript async updateStatus(status: 'ENABLED' | 'DISABLED', nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------- | ------------------------------------------- | | `status` | `'ENABLED' \| 'DISABLED'` | The status to set ('ENABLED' or 'DISABLED') | | `nanoid` | `string` | The unique identifier of the auth config | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated auth config details **Example** ```typescript // Disable an auth config await composio.authConfigs.updateStatus('DISABLED', 'auth_abc123'); // Enable an auth config await composio.authConfigs.updateStatus('ENABLED', 'auth_abc123'); ``` *** --- # Composio (/reference/sdk-reference/typescript/composio) ## Constructor [#constructor] ### constructor() [#constructor-1] Creates a new instance of the Composio SDK. The constructor initializes the SDK with the provided configuration options, sets up the API client, and initializes all core models (tools, toolkits, etc.). ```typescript constructor(config?: ComposioConfig): Composio ``` **Parameters** | Name | Type | Description | | --------- | ---------------- | ------------------------------------------ | | `config?` | `ComposioConfig` | Configuration options for the Composio SDK | **Returns** `Composio` **Example** ```typescript // Initialize with default configuration const composio = new Composio(); // Initialize with custom API key and base URL const composio = new Composio({ apiKey: 'your-api-key', baseURL: 'https://api.composio.dev' }); // Initialize with custom provider const composio = new Composio({ apiKey: 'your-api-key', provider: new CustomProvider() }); ``` *** ## Properties [#properties] | Name | Type | Description | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `authConfigs` | `AuthConfigs` | Manage authentication configurations for toolkits | | `connectedAccounts` | `ConnectedAccounts` | Manage authenticated connections | | `create` | `(userId: string, config: \{ authConfigs?: Record; connectedAccounts?: Record; experimental?: \{ assistivePrompt?: \{ userTimezone?: string \}; customToolkits?: CustomToolkit[]; customTools?: CustomTool[] \}; manageConnections?: boolean \| \{ callbackUrl?: string; enable?: boolean; waitForConnections?: boolean \}; mcp?: boolean; multiAccount?: \{ enable: boolean; maxAccountsPerToolkit?: number; requireExplicitSelection?: boolean \}; preload?: \{ tools?: ...[] \| 'all' \}; sandbox?: \{ autoOffloadThreshold?: number; enable: boolean; enableProxyExecution?: boolean; sandboxSize?: 'standard' \| 'medium' \| 'large' \| 'xlarge' \}; sessionPreset?: 'direct_tools'; tags?: ... \| ... \| ... \| ...[] \| \{ disable?: ...[]; enable?: ...[] \}; toolkits?: string[] \| \{ disable: ...[] \} \| \{ enable: ...[] \}; tools?: Record; workbench?: \{ autoOffloadThreshold?: number; enable: boolean; enableProxyExecution?: boolean; sandboxSize?: 'standard' \| 'medium' \| 'large' \| 'xlarge' \} \} & \{ mcp: true \}, requestOptions: ComposioRequestOptions) => Promise` | Creates a new tool router session for a user. | | Use `sessionPreset: SessionPreset.DIRECT_TOOLS` when all needed tools | | | | should be exposed directly; see `ToolRouterCreateSessionConfig`. | | | | `experimental` | `Experimental` | Experimental SDK methods whose shape may change in future releases. | | Prefer domain-specific mounts (for example | | | | `composio.connectedAccounts.updateAcl(...)`) when available; this | | | | namespace keeps compatibility aliases while APIs are experimental. | | | | Stateless experimental factories (e.g. `experimental_createTool`) stay | | | | at the top level. | | | | `files` | `Files` | Upload and download files | | `mcp` | `MCP` | Model Context Protocol server management. | | `provider` | `TProvider` | The tool provider instance used for wrapping tools in framework-specific formats | | `sessions` | `Sessions` | Create and reuse Composio sessions. | Prefer `composio.sessions.create(...)` for new code. The top-level `composio.create(...)` method is kept as an alias. | \| `toolkits` | `Toolkits` | Retrieve toolkit metadata and authorize user connections | \| `toolRouter` | `ToolRouter` | Legacy alias for `composio.sessions`. | \| `tools` | `Tools` | List, retrieve, and execute tools | \| `triggers` | `Triggers` | Manage webhook triggers and event subscriptions | \| `use` | `(id: string, options: \{ customToolkits?: CustomToolkit[]; customTools?: CustomTool[]; mcp: true \}, requestOptions: ComposioRequestOptions) => Promise` | Use an existing tool router session | ## Methods [#methods] ### createSession() (deprecated) [#createsession-deprecated] > **Deprecated**: Will be removed in a future version of the SDK. Instead, construct a new instance directly with the headers you need: `new Composio(\{ ...existingConfig, defaultHeaders \})`. For one-off overrides, pass per-call `requestOptions` where supported. Creates a new instance of the Composio SDK with custom request options while preserving the existing configuration. This method is particularly useful when you need to: * Add custom headers for specific requests * Track request contexts with unique identifiers * Override default request behavior for a subset of operations The new instance inherits all configuration from the parent instance (apiKey, baseURL, provider, etc.) but allows you to specify custom request options that will be used for all API calls made through this session. ```typescript createSession(options?: { headers?: ComposioRequestHeaders }): Composio ``` **Parameters** | Name | Type | | ---------- | ---------------------------------------- | | `options?` | `\{ headers?: ComposioRequestHeaders \}` | **Returns** `Composio` — A new Composio instance with the custom request options applied. **Example** ```typescript // Create a base Composio instance const composio = new Composio({ apiKey: 'your-api-key' }); // Create a session with request tracking headers const composioWithCustomHeaders = composio.createSession({ headers: { 'x-request-id': '1234567890', 'x-correlation-id': 'session-abc-123', 'x-custom-header': 'custom-value' } }); // Use the session for making API calls with the custom headers await composioWithCustomHeaders.tools.list(); ``` *** ### flush() [#flush] Flush any pending telemetry and wait for it to complete. In Node.js-compatible environments, telemetry is automatically flushed on process exit. However, in environments like Cloudflare Workers that don't support process exit events, you should call this method manually to ensure all telemetry is sent. ```typescript async flush(): Promise ``` **Returns** `Promise` — A promise that resolves when all pending telemetry has been sent. **Example** ```typescript // In a Cloudflare Worker, use ctx.waitUntil to ensure telemetry is flushed export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const composio = new Composio({ apiKey: env.COMPOSIO_API_KEY }); // Do your work... const result = await composio.tools.execute(...); // Ensure telemetry flushes before worker terminates ctx.waitUntil(composio.flush()); return new Response(JSON.stringify(result)); } }; ``` *** ### getClient() [#getclient] Get the Composio SDK client. ```typescript getClient(): ComposioClient ``` **Returns** `ComposioClient` — The Composio API client. *** ### getConfig() [#getconfig] Get the configuration SDK is initialized with. Returns a frozen shallow clone — the SDK has already snapshotted configuration values such as `dangerouslyAllowAutoUploadDownloadFiles`, `fileUploadDirs`, and `fileDownloadDir` into its internal models, so mutating the live config object would silently no-op. Freezing makes that contract visible at the call site instead of letting the mutation appear successful. ```typescript getConfig(): Readonly ``` **Returns** `Readonly` — The frozen configuration the SDK is initialized with. *** --- # ConnectedAccounts (/reference/sdk-reference/typescript/connected-accounts) ## Usage [#usage] Access this class through the `composio.connectedAccounts` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.connectedAccounts.list(); ``` ## Methods [#methods] ### delete() [#delete] Deletes a connected account. This method permanently removes a connected account from the Composio platform. This action cannot be undone and will revoke any access tokens associated with the account. ```typescript async delete(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account to delete | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The deletion response **Example** ```typescript // Delete a connected account await composio.connectedAccounts.delete('conn_abc123'); ``` *** ### disable() [#disable] Disable a connected account ```typescript async disable(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------ | | `nanoid` | `string` | Unique identifier of the connected account | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Updated connected account details **Example** ```typescript // Disable a connected account const disabledAccount = await composio.connectedAccounts.disable('conn_abc123'); console.log(disabledAccount.isDisabled); // true // You can also use updateStatus with a reason // const disabledAccount = await composio.connectedAccounts.updateStatus('conn_abc123', { // enabled: false, // reason: 'No longer needed' // }); ``` *** ### enable() [#enable] Enable a connected account ```typescript async enable(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------ | | `nanoid` | `string` | Unique identifier of the connected account | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Updated connected account details **Example** ```typescript // Enable a previously disabled connected account const enabledAccount = await composio.connectedAccounts.enable('conn_abc123'); console.log(enabledAccount.isDisabled); // false ``` *** ### get() [#get] Retrieves a specific connected account by its ID. This method fetches detailed information about a single connected account and transforms the response to the SDK's standardized format. ```typescript async get(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ---------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The connected account details **Example** ```typescript // Get a connected account by ID const account = await composio.connectedAccounts.get('conn_abc123'); console.log(account.status); // e.g., 'ACTIVE' console.log(account.toolkit.slug); // e.g., 'github' ``` *** ### initiate() [#initiate] Compound function to create a new connected account. This function creates a new connected account and returns a connection request. Users can then wait for the connection to be established using the `waitForConnection` method. **Deprecated for Composio-managed OAuth (OAuth1, OAuth2, DCR\_OAUTH).** The legacy `POST /api/v3/connected_accounts` endpoint that this method wraps is being retired for Composio-managed auth configs on redirectable schemes. The cutover is **2026-05-08** for new organizations and **2026-07-03** for all remaining organizations. After your org's cutover, this method will throw ComposioLegacyConnectedAccountsEndpointRetiredError for that specific combination. Use ConnectedAccounts.link for Composio-managed OAuth — it works for every redirectable scheme regardless of whether the auth config is Composio-managed or custom, and the return shape is the same. Custom auth configs (your own OAuth app) and non-OAuth schemes (API key, bearer token, basic auth) are unaffected and continue to work on `initiate()`. See [https://docs.composio.dev/docs/changelog/2026/04/24](https://docs.composio.dev/docs/changelog/2026/04/24) ```typescript async initiate(userId: string, authConfigId: string, options?: CreateConnectedAccountOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------- | -------------------------------------------- | | `userId` | `string` | User ID of the connected account | | `authConfigId` | `string` | Auth config ID of the connected account | | `options?` | `CreateConnectedAccountOptions` | Options for creating a new connected account | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Connection request object **Example** ```typescript // For OAuth2 authentication const connectionRequest = await composio.connectedAccounts.initiate( 'user_123', 'auth_config_123', { callbackUrl: 'https://your-app.com/callback', config: AuthScheme.OAuth2({ access_token: 'your_access_token', token_type: 'Bearer' }) } ); // For API Key authentication const connectionRequest = await composio.connectedAccounts.initiate( 'user_123', 'auth_config_123', { config: AuthScheme.ApiKey({ api_key: 'your_api_key' }) } ); // For Basic authentication const connectionRequest = await composio.connectedAccounts.initiate( 'user_123', 'auth_config_123', { config: AuthScheme.Basic({ username: 'your_username', password: 'your_password' }) } ); ``` *** ### link() [#link] ```typescript async link(userId: string, authConfigId: string, options?: CreateConnectedAccountLinkOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ----------------------------------- | ----------------------------------------------------------------------------------------- | | `userId` | `string` | \{string} - The external user ID to create the connected account for. | | `authConfigId` | `string` | \{string} - The auth config ID to create the connected account for. | | `options?` | `CreateConnectedAccountLinkOptions` | \{CreateConnectedAccountLinkOptions} - Options for creating a new connected account link. | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Connection request object **Example** ```typescript // create a connection request and redirect the user to the redirect url const connectionRequest = await composio.connectedAccounts.link('user_123', 'auth_config_123'); const redirectUrl = connectionRequest.redirectUrl; console.log(`Visit: ${redirectUrl} to authenticate your account`); // Wait for the connection to be established const connectedAccount = await connectionRequest.waitForConnection() ``` ```typescript // create a connection request and redirect the user to the redirect url const connectionRequest = await composio.connectedAccounts.link('user_123', 'auth_config_123', { callbackUrl: 'https://your-app.com/callback' }); const redirectUrl = connectionRequest.redirectUrl; console.log(`Visit: ${redirectUrl} to authenticate your account`); // Wait for the connection to be established const connectedAccount = await composio.connectedAccounts.waitForConnection(connectionRequest.id); ``` *** ### list() [#list] Lists all connected accounts based on provided filter criteria. This method retrieves connected accounts from the Composio API with optional filtering. ```typescript async list(query?: ConnectedAccountListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ---------------------------- | ---------------------------------------------------------- | | `query?` | `ConnectedAccountListParams` | Optional query parameters for filtering connected accounts | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — A paginated list of connected accounts **Example** ```typescript // List all connected accounts const allAccounts = await composio.connectedAccounts.list(); // List accounts for a specific user const userAccounts = await composio.connectedAccounts.list({ userIds: ['user123'] }); // List accounts for a specific toolkit const githubAccounts = await composio.connectedAccounts.list({ toolkitSlugs: ['github'] }); ``` *** ### refresh() [#refresh] Refreshes a connected account's authentication credentials. This method attempts to refresh OAuth tokens or other credentials associated with the connected account. This is useful when a token has expired or is about to expire. ```typescript async refresh(nanoid: string, options?: ConnectedAccountRefreshOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | -------------------------------- | --------------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account to refresh | | `options?` | `ConnectedAccountRefreshOptions` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The response containing the refreshed account details **Example** ```typescript // Refresh a connected account's credentials const refreshedAccount = await composio.connectedAccounts.refresh('conn_abc123'); ``` *** ### update() [#update] Enable or disable a connected account. Accepts `{ enabled: boolean }`. Use `updateAcl()` for ACL writes on SHARED connections. ```typescript async update(nanoid: string, params: UpdateConnectedAccountParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------ | ---------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account | | `params` | `UpdateConnectedAccountParams` | The update parameters | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The update response **Example** ```typescript // Disable an account await composio.connectedAccounts.update('ca_abc123', { enabled: false }); ``` *** ### updateAcl() [#updateacl] Update the per-user ACL on a SHARED connected account. **Experimental — shape may change in future releases.** Only meaningful for SHARED connections — calling this on a PRIVATE connection raises `ComposioAclOnlyForSharedError` (400). ACL writes require the connection's creator or an API key. PATCH semantics: omit a field to leave it unchanged; pass an empty array to clear an allow/deny list. At least one field must be provided. ```typescript async updateAcl(nanoid: string, params: UpdateConnectedAccountAclParams): Promise ``` **Parameters** | Name | Type | Description | | -------- | --------------------------------- | ---------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account | | `params` | `UpdateConnectedAccountAclParams` | The ACL fields to patch | **Returns** `Promise` — The PATCH response **Example** ```typescript // Allow every userId to use this SHARED connection await composio.connectedAccounts.updateAcl('ca_abc123', { allowAllUsers: true }); // Targeted allow list await composio.connectedAccounts.updateAcl('ca_abc123', { allowedUserIds: ['user_alice', 'user_bob'], }); // Clear the allow list (back to deny-by-default unless allowAllUsers is true) await composio.connectedAccounts.updateAcl('ca_abc123', { allowedUserIds: [] }); ``` *** ### updateStatus() [#updatestatus] Update the status of a connected account ```typescript async updateStatus(nanoid: string, params: ConnectedAccountUpdateStatusParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------------ | ------------------------------------------ | | `nanoid` | `string` | Unique identifier of the connected account | | `params` | `ConnectedAccountUpdateStatusParams` | Parameters for updating the status | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Updated connected account details **Example** ```typescript // Enable a connected account const updatedAccount = await composio.connectedAccounts.updateStatus('conn_abc123', { enabled: true }); // Disable a connected account with a reason const disabledAccount = await composio.connectedAccounts.updateStatus('conn_abc123', { enabled: false, reason: 'Token expired' }); ``` *** ### waitForConnection() [#waitforconnection] Waits for a connection request to complete and become active. This method continuously polls the Composio API to check the status of a connection until it either becomes active, enters a terminal error state, or times out. ```typescript async waitForConnection(connectedAccountId: string, timeout?: number): Promise ``` **Parameters** | Name | Type | Description | | -------------------- | -------- | ---------------------------------------------------------- | | `connectedAccountId` | `string` | The ID of the connected account to wait for | | `timeout?` | `number` | Maximum time to wait in milliseconds (default: 60 seconds) | **Returns** `Promise` — The finalized connected account data **Example** ```typescript // Wait for a connection to complete with default timeout const connectedAccount = await composio.connectedAccounts.waitForConnection('conn_123abc'); // Wait with a custom timeout of 2 minutes const connectedAccount = await composio.connectedAccounts.waitForConnection('conn_123abc', 120000); ``` *** --- # Experimental (/reference/sdk-reference/typescript/experimental) ## Usage [#usage] Access this class through the `composio.experimental` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.experimental.list(); ``` ## Methods [#methods] ### updateAcl() (deprecated) [#updateacl-deprecated] > **Deprecated**: Use `composio.connectedAccounts.updateAcl(...)` instead — ACL updates graduated onto the `connectedAccounts` mount. This experimental alias is kept only for backwards compatibility and will be removed once the API graduates. Prefer the `connectedAccounts` mount; do not generate new code against this alias. Compatibility alias for `composio.connectedAccounts.updateAcl(...)`. Update the per-user ACL on a SHARED connected account. **Experimental — shape may change in future releases.** Only meaningful for SHARED connections — calling this on a PRIVATE connection raises `ComposioAclOnlyForSharedError` (400). ACL writes require the connection's creator or an API key. PATCH semantics: omit a field to leave it unchanged; pass an empty array to clear an allow/deny list. At least one field must be provided. Resolution rule (deny wins): 1. requesting `userId` in `notAllowedUserIds` → DENY 2. `allowAllUsers === true` → ALLOW 3. requesting `userId` in `allowedUserIds` → ALLOW 4. otherwise → DENY ```typescript async updateAcl(nanoid: string, params: UpdateConnectedAccountAclParams): Promise ``` **Parameters** | Name | Type | | -------- | --------------------------------- | | `nanoid` | `string` | | `params` | `UpdateConnectedAccountAclParams` | **Returns** `Promise` — The PATCH response (`\{ id, status, success \}`). To read the updated ACL block, call `composio.connectedAccounts.get(nanoid)` after the promise resolves and inspect `account.experimental?.aclConfigForShared`. **Example** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: '...' }); // Allow every userId to use this connection await composio.connectedAccounts.updateAcl('ca_abc', { allowAllUsers: true }); // Everyone except a specific user await composio.connectedAccounts.updateAcl('ca_abc', { allowAllUsers: true, notAllowedUserIds: ['user_bob'], }); // Targeted allow await composio.connectedAccounts.updateAcl('ca_abc', { allowedUserIds: ['user_alice', 'user_bob'], }); // Revoke a previously-granted allow list (back to deny-by-default) await composio.connectedAccounts.updateAcl('ca_abc', { allowedUserIds: [] }); ``` **Empty-array semantics — read carefully.** Passing `[]` for either list **replaces** the list, it does not extend it: * `allowedUserIds: []` → revoke all previously-granted user IDs (state reverts to deny-by-default unless `allowAllUsers` is true). * `notAllowedUserIds: []` → **clears the deny list**, which silently re-grants access to users you previously blocked. Always pair an empty deny list with a deliberate audit of the allow side. ``` --- ``` --- # TypeScript SDK Reference (/reference/sdk-reference/typescript) ## Installation [#installation] ## Classes [#classes] | Class | Description | | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | [`Composio`](/reference/sdk-reference/typescript/composio) | This is the core class for Composio. | | [`AuthConfigs`](/reference/sdk-reference/typescript/auth-configs) | AuthConfigs class | | [`ConnectedAccounts`](/reference/sdk-reference/typescript/connected-accounts) | ConnectedAccounts class | | [`Experimental`](/reference/sdk-reference/typescript/experimental) | Experimental API | | [`MCP`](/reference/sdk-reference/typescript/mcp) | MCP (Model Control Protocol) class | | [`RemoteFile`](/reference/sdk-reference/typescript/remote-file) | Represents a file stored in a tool router session's file mount. | | [`Sessions`](/reference/sdk-reference/typescript/sessions) | First-class API for creating and reusing Composio sessions. | | [`Toolkits`](/reference/sdk-reference/typescript/toolkits) | Toolkits class | | [`Session`](/reference/sdk-reference/typescript/session) | A Composio session — the object returned by `composio.sessions.create(...)` | | [`Session files`](/reference/sdk-reference/typescript/session-files) | File mount for a Composio session, reached via `session.experimental.files` | | [`Tools`](/reference/sdk-reference/typescript/tools) | This class is used to manage tools in the Composio SDK. | | [`Triggers`](/reference/sdk-reference/typescript/triggers) | Trigger (Instance) class | ## Quick Start [#quick-start] ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); // Get tools for a user const tools = await composio.tools.get('user-123', { toolkits: ['github'] }); // Execute a tool const result = await composio.tools.execute('GITHUB_GET_REPOS', { userId: 'user-123', arguments: { owner: 'composio' } }); ``` --- # MCP (/reference/sdk-reference/typescript/mcp) > **Deprecated**: Use a session's MCP endpoint instead: `composio.create(userId, \{ mcp: true \})` returns a session that exposes `session.mcp.url` / `session.mcp.headers`. MCP is now opt-in per session; the standalone `composio.mcp` server-management API is kept only for backwards compatibility. Prefer the session MCP endpoint; do not generate new code against `composio.mcp`. See [https://docs.composio.dev/docs/sessions-via-mcp](https://docs.composio.dev/docs/sessions-via-mcp) ## Usage [#usage] Access this class through the `composio.mcp` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.mcp.list(); ``` ## Properties [#properties] | Name | Type | | -------- | ---------- | | `client` | `Composio` | ## Methods [#methods] ### create() [#create] Create a new MCP configuration. ```typescript async create(name: string, mcpConfig: MCPConfigCreationParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------- | | `name` | `string` | | `mcpConfig` | `MCPConfigCreationParams` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` — Created server details with instance getter **Example** ```typescript const server = await composio.mcpConfig.create("personal-mcp-server", { toolkits: ["github", "slack"], allowedTools: ["GMAIL_FETCH_EMAILS", "SLACK_SEND_MESSAGE"], manuallyManageConnections: false } }); const server = await composio.mcpConfig.create("personal-mcp-server", { toolkits: [{ toolkit: "gmail", authConfigId: "ac_243434343" }], allowedTools: ["GMAIL_FETCH_EMAILS"], manuallyManageConnections: false } }); ``` *** ### delete() [#delete] Delete an MCP server configuration permanently ```typescript async delete(serverId: string, requestOptions?: ComposioRequestOptions): Promise<{ id: string; deleted: boolean }> ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------- | | `serverId` | `string` | The unique identifier of the MCP server to delete | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise<\{ id: string; deleted: boolean \}>` — Confirmation object with server ID and deletion status **Example** ```typescript // Delete an MCP server by ID const result = await composio.experimental.mcp.delete("mcp_12345"); if (result.deleted) { console.log(`Server ${result.id} has been successfully deleted`); } else { console.log(`Failed to delete server ${result.id}`); } // Example with error handling try { const result = await composio.experimental.mcp.delete("mcp_12345"); console.log("Deletion successful:", result); } catch (error) { console.error("Failed to delete MCP server:", error.message); } // Delete and verify from list await composio.experimental.mcp.delete("mcp_12345"); const servers = await composio.experimental.mcp.list({}); const serverExists = servers.items.some(server => server.id === "mcp_12345"); console.log("Server still exists:", serverExists); // Should be false ``` *** ### generate() [#generate] Get server URLs for an existing MCP server. The response is wrapped according to the provider's specifications. ```typescript async generate(userId: string, mcpConfigId: string, options?: MCPGetInstanceParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------------------------------------ | | `userId` | `string` | \{string} external user id from your database for whom you want the server for | | `mcpConfigId` | `string` | \{string} config id of the MCPConfig for which you want to create a server for | | `options?` | `MCPGetInstanceParams` | \{object} additional options | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` **Example** ```typescript import { Composio } from "@composio/code"; const composio = new Composio(); const mcp = await composio.experimental.mcp.generate("default", ""); ``` *** ### get() [#get] Retrieve detailed information about a specific MCP server by its ID ```typescript async get(serverId: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------------------- | | `serverId` | `string` | The unique identifier of the MCP server to retrieve | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Complete MCP server details including configuration, tools, and metadata **Example** ```typescript // Get a specific MCP server by ID const server = await composio.experimental.mcp.get("mcp_12345"); console.log(server.name); // "My Personal MCP Server" console.log(server.allowedTools); // ["GITHUB_CREATE_ISSUE", "SLACK_SEND_MESSAGE"] console.log(server.toolkits); // ["github", "slack"] console.log(server.serverInstanceCount); // 3 // Access setup commands for different clients console.log(server.commands.claude); // Claude setup command console.log(server.commands.cursor); // Cursor setup command console.log(server.commands.windsurf); // Windsurf setup command // Use the MCP URL for direct connections const mcpUrl = server.MCPUrl; ``` *** ### list() [#list] List the MCP servers with optional filtering and pagination ```typescript async list(options: MCPListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------- | | `options` | `MCPListParams` | Filtering and pagination options | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Paginated list of MCP servers with metadata **Example** ```typescript // List all MCP servers const allServers = await composio.experimental.mcp.list({}); // List with pagination const pagedServers = await composio.experimental.mcp.list({ page: 2, limit: 5 }); // Filter by toolkit const githubServers = await composio.experimental.mcp.list({ toolkits: ['github', 'slack'] }); // Filter by name const namedServers = await composio.experimental.mcp.list({ name: 'personal' }); ``` *** ### update() [#update] Update an existing MCP server configuration with new settings ```typescript async update(serverId: string, config: MCPUpdateParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------- | | `serverId` | `string` | The unique identifier of the MCP server to update | | `config` | `MCPUpdateParams` | Update configuration parameters | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Updated MCP server configuration with all details **Example** ```typescript // Update server name only const updatedServer = await composio.experimental.mcp.update("mcp_12345", { name: "My Updated MCP Server" }); // Update toolkits and tools const serverWithNewTools = await composio.experimental.mcp.update("mcp_12345", { toolkits: [ { toolkit: "github", authConfigId: "auth_abc123", allowedTools: ["GITHUB_CREATE_ISSUE", "GITHUB_LIST_REPOS"] }, { toolkit: "slack", authConfigId: "auth_xyz789", allowedTools: ["SLACK_SEND_MESSAGE", "SLACK_LIST_CHANNELS"] } ] }); // Update connection management setting const serverWithManualAuth = await composio.experimental.mcp.update("mcp_12345", { name: "Manual Auth Server", manuallyManageConnections: true }); // Complete update example const fullyUpdatedServer = await composio.experimental.mcp.update("mcp_12345", { name: "Production MCP Server", toolkits: [ { toolkit: "gmail", authConfigId: "auth_gmail_prod", } ], allowedTools: ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"] manuallyManageConnections: false }); console.log("Updated server:", fullyUpdatedServer.name); console.log("New tools:", fullyUpdatedServer.allowedTools); ``` *** --- # RemoteFile (/reference/sdk-reference/typescript/remote-file) ## Usage [#usage] Access this class through the `composio.remoteFile` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.remoteFile.list(); ``` ## Properties [#properties] | Name | Type | Description | | -------------------- | -------- | -------------------------------------------------------- | | `downloadUrl` | `string` | Presigned URL for downloading the file | | `expiresAt` | `string` | ISO 8601 timestamp when the download URL expires | | `mountRelativePath` | `string` | Relative path within the mount (e.g. "report.pdf") | | `sandboxMountPrefix` | `string` | Absolute mount path inside the sandbox (e.g. /mnt/files) | ## Methods [#methods] ### blob() [#blob] Fetches the file content as a Blob. ```typescript async blob(): Promise ``` **Returns** `Promise` — The file content as a Blob *** ### buffer() [#buffer] Fetches the file content as a buffer. ```typescript async buffer(): Promise ``` **Returns** `Promise` — The file content as a Uint8Array *** ### save() [#save] Downloads and saves the file to the local filesystem. Requires a Node.js runtime with file system support (not available in Cloudflare Workers/Edge). ```typescript async save(path?: string): Promise ``` **Parameters** | Name | Type | Description | | ------- | -------- | --------------------------------------------------------------------------------------------------------------------- | | `path?` | `string` | Local path to save the file. If omitted, saves to the Composio temp directory using the filename from the mount path. | **Returns** `Promise` — The absolute path where the file was saved *** ### text() [#text] Fetches the file content as UTF-8 text. ```typescript async text(): Promise ``` **Returns** `Promise` — The file content as a string *** ### parse() [#parse] Parses an API response (snake\_case) and returns a RemoteFile instance. ```typescript parse(data: unknown): RemoteFile ``` **Parameters** | Name | Type | Description | | ------ | --------- | -------------------------------------- | | `data` | `unknown` | Raw API response with snake\_case keys | **Returns** `RemoteFile` — A RemoteFile instance *** --- # Session files (/reference/sdk-reference/typescript/session-files) ## Methods [#methods] ### delete() [#delete] Deletes a file or directory at the specified path on the session's file mount. Removes the file or directory from the virtual filesystem. Use with caution: deletion is typically irreversible. Ensure the path exists and is intended for removal. ```typescript async delete(remotePath: string, options?: ToolRouterSessionFilesMountDeleteOptions): Promise ``` **Parameters** | Name | Type | Description | | ------------ | ------------------------------------------ | --------------------------------------------------------- | | `remotePath` | `string` | The path of the file or directory to delete on the mount. | | `options?` | `ToolRouterSessionFilesMountDeleteOptions` | Optional configuration for the delete operation. | **Returns** `Promise` — Confirmation of deletion (implementation-specific). **Example** ```typescript const session = await composio.toolRouter.use('session_123'); await session.experimental.files.delete('/temp/cache.json'); ``` ```typescript // Delete from a custom mount await session.experimental.files.delete('/old-backup', { mountId: 'custom-mount', }); ``` *** ### download() [#download] Downloads a file from the session's file mount to the local filesystem. Retrieves a file stored in the session's virtual filesystem (e.g., one produced by a tool or previously uploaded) and saves it to the specified local path. ```typescript async download(filePath: string, options?: ToolRouterSessionFilesMountDownloadOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `filePath` | `string` | The path of the file on the mount to download, or the local path where the file should be saved (implementation-specific). | | `options?` | `ToolRouterSessionFilesMountDownloadOptions` | Optional configuration for the download. | **Returns** `Promise` — The downloaded file data or path (implementation-specific). **Example** ```typescript const session = await composio.toolRouter.use('session_123'); const result = await session.experimental.files.download('/output/report.pdf'); ``` ```typescript // Download from a custom mount await session.experimental.files.download('/exports/data.json', { mountId: 'custom-mount', }); ``` *** ### list() [#list] Lists files and directories at the specified path on the session's file mount. Use this to browse the virtual filesystem attached to the tool router session. The path is relative to the mount root (e.g., `"/"` for root, `"/documents"` for a subdirectory). Supports cursor-based pagination via `cursor` and `limit` options. ```typescript async list(options?: ToolRouterSessionFilesMountListOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | ---------------------------------------- | ---------------------------------------------- | | `options?` | `ToolRouterSessionFilesMountListOptions` | Optional configuration for the list operation. | **Returns** `Promise` — List of files with nextCursor for pagination. **Example** ```typescript const session = await composio.toolRouter.use('session_123'); const { items, nextCursor } = await session.experimental.files.list({ path: '/' }); ``` ```typescript // Paginated listing let result = await session.experimental.files.list({ path: '/', limit: 10 }); while (result.nextCursor) { result = await session.experimental.files.list({ path: '/', cursor: result.nextCursor, limit: 10 }); } ``` *** ### upload() [#upload] Uploads a file to the session's file mount. Accepts a file path (local or URL), a native File object, or a raw buffer. The file is stored in the virtual filesystem associated with the tool router session. URL inputs require a Node.js or Bun runtime so the destination can be DNS-validated; edge runtimes must fetch the file themselves and pass a File or ArrayBuffer. ```typescript async upload(input: string | File | ArrayBuffer | Uint8Array, options?: ToolRouterSessionFilesMountUploadOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | --------------------------------------------- | --------------------------------------------------------------------------- | | `input` | `string \| File \| ArrayBuffer \| Uint8Array` | File path (string), native File, or raw buffer (ArrayBuffer \| Uint8Array). | | `options?` | `ToolRouterSessionFilesMountUploadOptions` | Optional configuration. When passing a buffer, remotePath is required. | **Returns** `Promise` — Metadata about the uploaded file. **Example** ```typescript // From file path (local or URL) await session.experimental.files.upload('/path/to/report.pdf'); await session.experimental.files.upload('https://example.com/file.pdf'); ``` ```typescript // From native File (e.g. from input[type=file]) await session.experimental.files.upload(fileInput.files[0]); ``` ```typescript // From raw buffer await session.experimental.files.upload(buffer, { remotePath: 'data.json', mimetype: 'application/json' }); ``` *** --- # Session (/reference/sdk-reference/typescript/session) ## Properties [#properties] | Name | Type | Description | | --------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `configVersion` | `number` | | | `experimental` | `SessionExperimental` | | | `mcp` | `\{ headers?: Record; type: 'http' \| 'sse'; url: string \}` | Hosted MCP endpoint (`session.mcp.url` / `session.mcp.headers`). Exists on every session at runtime, but only surfaced in the type when the session is created with `\{ mcp: true \}` (which returns `Session`); the default `SessionWithoutMcp` omits `mcp`, so MCP is an explicit opt-in. See [https://docs.composio.dev/docs/sessions-via-mcp](https://docs.composio.dev/docs/sessions-via-mcp) | | `preload` | `Preload` | | | `sandbox` | `Workbench` | Resolved sandbox (code-execution) config returned by the API. `enable` defaults to `true` server-side. | | `sessionId` | `string` | | | `warnings` | `Warning[]` | | ## Methods [#methods] ### authorize() [#authorize] Initiate an authorization flow for a toolkit. Returns a ConnectionRequest with a redirect URL for the user. Pass `experimental: { accountType: 'SHARED', aclConfigForShared }` to create a SHARED connection with a per-user ACL in one flow. Default behaviour (omit the block) creates a PRIVATE connection. Experimental — shape may change in future releases. `aclConfigForShared` is validated against the same caps as `composio.connectedAccounts.link()` (≤1000 entries per list, each `userId` 1..256 characters). Invalid input throws `ValidationError` at the SDK boundary. ```typescript async authorize(toolkit: string, options?: { callbackUrl?: string; alias?: string; experimental?: { accountType?: ConnectedAccountType; aclConfigForShared?: ConnectedAccountAclConfig; }; }, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `toolkit` | `string` | | `options?` | `\{ callbackUrl?: string; alias?: string; experimental?: \{ accountType?: ConnectedAccountType; aclConfigForShared?: ConnectedAccountAclConfig; \}; \}` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ### customToolkits() [#customtoolkits] List all custom toolkits registered in this session. Returns toolkits with their tools showing final slugs. ```typescript customToolkits(): RegisteredCustomToolkit[] ``` **Returns** `RegisteredCustomToolkit[]` — Array of registered custom toolkits *** ### customTools() [#customtools] List all custom tools registered in this session. Returns tools with their final slugs, schemas, and resolved toolkit. ```typescript customTools(options?: { toolkit?: string }): RegisteredCustomTool[] ``` **Parameters** | Name | Type | | ---------- | ------------------------ | | `options?` | `\{ toolkit?: string \}` | **Returns** `RegisteredCustomTool[]` — Array of registered custom tools *** ### delete() [#delete] Delete this session. Deleted sessions immediately stop being retrievable or executable. Deleting an already-deleted session surfaces the backend 404. ```typescript async delete(requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ### execute() [#execute] Execute a tool within the session. For custom tools, accepts the full slug (e.g. "LOCAL\_GREP") or the original slug (e.g. "GREP") when that original slug is unique across the session's custom tools and toolkits. Custom tools are executed in-process; remote tools are sent to the Composio backend. ```typescript async execute(toolSlug: string, arguments_?: Record, options?: ToolRouterSessionExecuteOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | --------------------------------- | -------------------------- | | `toolSlug` | `string` | The tool slug to execute | | `arguments_?` | `Record` | Optional tool arguments | | `options?` | `ToolRouterSessionExecuteOptions` | Optional execution options | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The tool execution result *** ### proxyExecute() [#proxyexecute] Proxy an API call through Composio's auth layer using the session's connected account. The backend resolves the connected account from the toolkit within the session. ```typescript async proxyExecute(params: SessionProxyExecuteParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | --------------------------- | -------------------------------------------------------------------------------- | | `params` | `SessionProxyExecuteParams` | Proxy request parameters (toolkit, endpoint, method, body, headers/query params) | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The proxied API response with status, data, headers *** ### search() [#search] Search for tools by semantic use case. Returns relevant tools for the given query with schemas and guidance. ```typescript async search(params: { query: string; toolkits?: string[]; }, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------------------------- | | `params` | `\{ query: string; toolkits?: string[]; \}` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ### toolkits() [#toolkits] Query the connection state of toolkits in the session. Supports pagination and filtering by toolkit slugs. ```typescript async toolkits(options?: ToolRouterToolkitsOptions, requestOptions?: ComposioRequestOptions): Promise<{ cursor: string | undefined; items: { connection?: { authConfig?: ... | ...; connectedAccount?: { id: ...; status: ... }; isActive: boolean }; isNoAuth: boolean; logo?: string; name: string; slug: string }[]; totalPages: number }> ``` **Parameters** | Name | Type | | ----------------- | --------------------------- | | `options?` | `ToolRouterToolkitsOptions` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise<\{ cursor: string \| undefined; items: \{ connection?: \{ authConfig?: ... \| ...; connectedAccount?: \{ id: ...; status: ... \}; isActive: boolean \}; isNoAuth: boolean; logo?: string; name: string; slug: string \}[]; totalPages: number \}>` *** ### tools() [#tools] Get the tools available in the session, formatted for your AI framework. Requires a provider to be configured in the Composio constructor. When custom tools are bound to the session, execution of COMPOSIO\_MULTI\_EXECUTE\_TOOL is intercepted: local tools are executed in-process, remote tools are sent to the backend. ```typescript async tools(modifiers?: SessionMetaToolOptions, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `modifiers?` | `SessionMetaToolOptions` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise>` *** ### update() [#update] Partially update the session configuration. Only the fields provided will be changed; omitted fields are preserved. Mutates this session's `configVersion`, `preload`, and `warnings` in-place. ```typescript async update(config: ToolRouterUpdateSessionConfig, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------------- | | `config` | `ToolRouterUpdateSessionConfig` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** --- # Sessions (/reference/sdk-reference/typescript/sessions) ## Usage [#usage] Access this class through the `composio.sessions` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.sessions.list(); ``` ## Methods [#methods] ### create() [#create] Creates a new tool router session for a user. Use `sessionPreset: SessionPreset.DIRECT_TOOLS` when all needed tools should be exposed directly; see `ToolRouterCreateSessionConfig`. **Overload 1** ```typescript async create(userId: string, config: ToolRouterCreateSessionConfig & { mcp: true }, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------------------------- | ----------------------------------------------------------------------- | | `userId` | `string` | \{string} The user id to create the session for | | `config` | `ToolRouterCreateSessionConfig & \{ mcp: true \}` | \{ToolRouterCreateSessionConfig} The config for the tool router session | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise>` — The tool router session **Overload 2** ```typescript async create(userId: string, config?: ToolRouterCreateSessionConfig, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------- | ----------------------------------------------------------------------- | | `userId` | `string` | \{string} The user id to create the session for | | `config?` | `ToolRouterCreateSessionConfig` | \{ToolRouterCreateSessionConfig} The config for the tool router session | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise>` — The tool router session **Example** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.sessions.create('user_123', { toolkits: ['gmail'], manageConnections: true, experimental: { customTools: [myCustomTool], customToolkits: [myToolkit], }, }); ``` *** ### delete() [#delete] Delete a tool router session by ID. Deleted sessions immediately stop being retrievable or executable. Deleting a missing or already-deleted session surfaces the backend 404. ```typescript async delete(id: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `id` | `string` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ### use() [#use] Use an existing session **Overload 1** ```typescript async use(id: string, options: { customTools?: CustomTool[]; customToolkits?: CustomToolkit[]; mcp: true }, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------------------------------------------------------- | -------------------------------------- | | `id` | `string` | \{string} The id of the session to use | | `options` | `\{ customTools?: CustomTool[]; customToolkits?: CustomToolkit[]; mcp: true \}` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise>` — The tool router session **Overload 2** ```typescript async use(id: string, options?: { customTools?: CustomTool[]; customToolkits?: CustomToolkit[]; mcp?: boolean }, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ----------------- | ----------------------------------------------------------------------------------- | -------------------------------------- | | `id` | `string` | \{string} The id of the session to use | | `options?` | `\{ customTools?: CustomTool[]; customToolkits?: CustomToolkit[]; mcp?: boolean \}` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise>` — The tool router session **Example** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const id = 'session_123'; const session = await composio.sessions.use(id); console.log(session.mcp.url); console.log(session.mcp.headers); ``` *** --- # Toolkits (/reference/sdk-reference/typescript/toolkits) ## Usage [#usage] Access this class through the `composio.toolkits` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.toolkits.list(); ``` ## Methods [#methods] ### authorize() [#authorize] Authorizes a user to use a toolkit. This method will create an auth config if one doesn't exist and initiate a connection request. ```typescript async authorize(userId: string, toolkitSlug: string, authConfigId?: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------ | | `userId` | `string` | The user id of the user to authorize | | `toolkitSlug` | `string` | The slug of the toolkit to authorize | | `authConfigId?` | `string` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The connection request object **Example** ```typescript const connectionRequest = await composio.toolkits.authorize(userId, 'github'); ``` *** ### get() [#get] Retrieves a specific toolkit by its slug identifier. **Overload 1** ```typescript async get(slug: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ----------------------------------------------------- | | `slug` | `string` | The unique slug identifier of the toolkit to retrieve | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The toolkit object with detailed information **Overload 2** ```typescript async get(query?: ToolkitListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------- | | `query?` | `ToolkitListParams` | The query parameters to filter toolkits | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — A paginated list of toolkits matching the query criteria **Example** ```typescript // Get a specific toolkit const githubToolkit = await composio.toolkits.get('github'); console.log(githubToolkit.name); // GitHub console.log(githubToolkit.authConfigDetails); // Authentication configuration details ``` *** ### getAuthConfigCreationFields() [#getauthconfigcreationfields] Retrieves the fields required for creating an auth config for a toolkit. ```typescript async getAuthConfigCreationFields(toolkitSlug: string, authScheme: AuthSchemeType, options: { requiredOnly?: boolean }): Promise ``` **Parameters** | Name | Type | Description | | ------------- | ------------------------------ | -------------------------------------------------- | | `toolkitSlug` | `string` | The slug of the toolkit to retrieve the fields for | | `authScheme` | `AuthSchemeType` | The auth scheme to retrieve the fields for | | `options` | `\{ requiredOnly?: boolean \}` | | **Returns** `Promise` — The fields required for creating an auth config *** ### getConnectedAccountInitiationFields() [#getconnectedaccountinitiationfields] Retrieves the fields required for initiating a connected account for a toolkit. ```typescript async getConnectedAccountInitiationFields(toolkitSlug: string, authScheme: AuthSchemeType, options: { requiredOnly?: boolean }): Promise ``` **Parameters** | Name | Type | Description | | ------------- | ------------------------------ | -------------------------------------------------- | | `toolkitSlug` | `string` | The slug of the toolkit to retrieve the fields for | | `authScheme` | `AuthSchemeType` | The auth scheme to retrieve the fields for | | `options` | `\{ requiredOnly?: boolean \}` | | **Returns** `Promise` — The fields required for initiating a connected account *** ### listCategories() [#listcategories] Retrieves all toolkit categories available in the Composio SDK. This method fetches the complete list of categories from the Composio API and transforms the response to use camelCase property naming. ```typescript async listCategories(requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` — The list of toolkit categories **Example** ```typescript // Get all toolkit categories const categories = await composio.toolkits.listCategories(); console.log(categories.items); // Array of category objects ``` *** --- # Tools (/reference/sdk-reference/typescript/tools) ## Usage [#usage] Access this class through the `composio.tools` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.tools.list(); ``` ## Methods [#methods] ### execute() [#execute] Executes a given tool with the provided parameters. This method calls the Composio API to execute the tool and returns the response. **Version Control:** By default, manual tool execution requires a specific toolkit version. If the version resolves to "latest", the execution will throw a `ComposioToolVersionRequiredError` unless `dangerouslySkipVersionCheck` is set to `true`. This helps prevent unexpected behavior when new toolkit versions are released. ```typescript async execute(slug: string, body: ToolExecuteParams, options?: ExecuteToolModifiers & ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | ----------------------------------------------- | --------------------------------------- | | `slug` | `string` | The slug/ID of the tool to be executed | | `body` | `ToolExecuteParams` | The parameters to be passed to the tool | | `options?` | `ExecuteToolModifiers & ComposioRequestOptions` | Optional modifiers and request options | **Returns** `Promise` — The response from the tool execution **Example** ```typescript const result = await composio.tools.execute('GITHUB_GET_REPOS', { userId: 'default', version: '20250909_00', arguments: { owner: 'composio' } }); ``` ```typescript const result = await composio.tools.execute('HACKERNEWS_GET_USER', { userId: 'default', arguments: { userId: 'pg' }, dangerouslySkipVersionCheck: true // Allows execution with "latest" version }); ``` ```typescript // If toolkitVersions are set during Composio initialization, no need to pass version const composio = new Composio({ toolkitVersions: { github: '20250909_00' } }); const result = await composio.tools.execute('GITHUB_GET_REPOS', { userId: 'default', arguments: { owner: 'composio' } }); ``` ```typescript const result = await composio.tools.execute('GITHUB_GET_ISSUES', { userId: 'default', version: '20250909_00', arguments: { owner: 'composio', repo: 'sdk' } }, { beforeExecute: ({ toolSlug, toolkitSlug, params }) => { console.log(`Executing ${toolSlug} from ${toolkitSlug}`); return params; }, afterExecute: ({ toolSlug, toolkitSlug, result }) => { console.log(`Completed ${toolSlug}`); return result; } }); ``` ```typescript const result = await composio.tools.execute('HACKERNEWS_GET_FRONTPAGE', { userId: 'default', arguments: {}, dangerouslySkipVersionCheck: true, }, { signal: AbortSignal.timeout(5_000) }); ``` *** ### executeSessionTool() [#executesessiontool] Executes a tool based on a tool router session. ```typescript async executeSessionTool(toolSlug: string, body: ToolExecuteMetaParams, modifiers?: SessionExecuteMetaModifiers, tool?: Tool, options?: ToolRouterSessionExecuteOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | --------------------------------- | ------------------------------------------------------------------- | | `toolSlug` | `string` | The slug of the tool to execute | | `body` | `ToolExecuteMetaParams` | The execution parameters | | `modifiers?` | `SessionExecuteMetaModifiers` | The modifiers to apply to the tool | | `tool?` | `Tool` | Optional tool schema used to resolve toolkit metadata for modifiers | | `options?` | `ToolRouterSessionExecuteOptions` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The response from the tool execution *** ### get() [#get] Get a list of tools from Composio based on filters. This method fetches the tools from the Composio API and wraps them using the provider. **Overload 1** ```typescript async get(userId: string, filters: ToolListParams, options?: ProviderOptions & ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ---------- | ------------------------------------------ | ----------------------------------------------- | | `userId` | `string` | The user id to get the tools for | | `filters` | `ToolListParams` | The filters to apply when fetching tools | | `options?` | `ProviderOptions & ComposioRequestOptions` | Provider options, modifiers, and/or AbortSignal | **Returns** `Promise>` — The wrapped tools collection **Overload 2** ```typescript async get(userId: string, slug: string, options?: ProviderOptions & ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ---------- | ------------------------------------------ | -------------------------------------------------------- | | `userId` | `string` | The user id to get the tool for | | `slug` | `string` | The slug of the tool to fetch | | `options?` | `ProviderOptions & ComposioRequestOptions` | Optional provider options including modifiers and signal | **Returns** `Promise>` — The wrapped tool **Example** ```typescript // Get tools from the GitHub toolkit const tools = await composio.tools.get('default', { toolkits: ['github'], limit: 10 }); // Timeout a slow search after 5s const emailTools = await composio.tools.get('default', { search: 'send email', }, { signal: AbortSignal.timeout(5_000) }); ``` *** ### getInput() [#getinput] Fetches the input parameters for a given tool. This method is used to get the input parameters for a tool before executing it. ```typescript async getInput(slug: string, body: ToolGetInputParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------- | | `slug` | `string` | The ID of the tool to find input for | | `body` | `ToolGetInputParams` | The parameters to be passed to the tool | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The input parameters schema for the specified tool **Example** ```typescript // Get input parameters for a specific tool const inputParams = await composio.tools.getInput('GITHUB_CREATE_ISSUE', { userId: 'default' }); console.log(inputParams.schema); ``` *** ### getRawComposioToolBySlug() [#getrawcomposiotoolbyslug] Retrieves a specific tool by its slug from the Composio API. This method fetches a single tool in raw format without provider-specific wrapping, providing direct access to the tool's schema and metadata. Tool versions are controlled at the Composio SDK initialization level through the `toolkitVersions` configuration. Local experimental custom tools are session-scoped; attach them when creating or reusing a Tool Router session, then use `session.tools()`, `session.customTools()`, or `session.execute()`. ```typescript async getRawComposioToolBySlug(slug: string, options?: ToolRetrievalOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------------------- | | `slug` | `string` | The unique identifier of the tool (e.g., 'GITHUB\_GET\_REPOS') | | `options?` | `ToolRetrievalOptions` | Optional configuration for tool retrieval | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The requested tool with its complete schema and metadata **Example** ```typescript // Get a tool by slug const tool = await composio.tools.getRawComposioToolBySlug('GITHUB_GET_REPOS'); console.log(tool.name, tool.description); // Get a tool with schema transformation const customizedTool = await composio.tools.getRawComposioToolBySlug( 'SLACK_SEND_MESSAGE', { modifySchema: ({ toolSlug, toolkitSlug, schema }) => { return { ...schema, description: `Enhanced ${schema.description} with custom modifications`, customMetadata: { lastModified: new Date().toISOString(), toolkit: toolkitSlug } }; } } ); // Access tool properties const githubTool = await composio.tools.getRawComposioToolBySlug('GITHUB_CREATE_ISSUE'); console.log({ slug: githubTool.slug, name: githubTool.name, toolkit: githubTool.toolkit?.name, version: githubTool.version, availableVersions: githubTool.availableVersions, inputParameters: githubTool.inputParameters }); ``` *** ### getRawComposioTools() [#getrawcomposiotools] Lists Composio API tools available to the SDK. This method fetches remote Composio tools from the API in raw format. The response can be filtered and modified as needed. Local experimental custom tools are session-scoped; attach them when creating or reusing a Tool Router session, then use `session.tools()`, `session.customTools()`, or `session.execute()`. It provides access to the underlying tool data without provider-specific wrapping. ```typescript async getRawComposioTools(query: ToolListParams, options?: SchemaModifierOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ----------------------------------------------- | | `query` | `ToolListParams` | Query parameters to filter the tools (required) | | `options?` | `SchemaModifierOptions` | Optional configuration for tool retrieval | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — List of tools matching the query criteria **Example** ```typescript // Get tools from specific toolkits const githubTools = await composio.tools.getRawComposioTools({ toolkits: ['github'], limit: 10 }); // Get specific tools by slug const specificTools = await composio.tools.getRawComposioTools({ tools: ['GITHUB_GET_REPOS', 'HACKERNEWS_GET_USER'] }); // Get tools from specific toolkits const githubTools = await composio.tools.getRawComposioTools({ toolkits: ['github'], limit: 10 }); // Get tools with schema transformation const customizedTools = await composio.tools.getRawComposioTools({ toolkits: ['github'], limit: 5 }, { modifySchema: ({ toolSlug, toolkitSlug, schema }) => { // Add custom properties to tool schema return { ...schema, customProperty: `Modified ${toolSlug} from ${toolkitSlug}`, tags: [...(schema.tags || []), 'customized'] }; } }); // Search for tools const searchResults = await composio.tools.getRawComposioTools({ search: 'user management' }); // Get tools by authentication config const authSpecificTools = await composio.tools.getRawComposioTools({ authConfigIds: ['auth_config_123'] }); ``` *** ### getRawToolRouterSessionTools() [#getrawtoolroutersessiontools] Fetches tools exposed by a tool router session. This includes helper/meta tools plus any tools preloaded into the session. It provides access to the underlying tool data without provider-specific wrapping. ```typescript async getRawToolRouterSessionTools(sessionId: string, options?: SchemaModifierOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------------------------ | | `sessionId` | `string` | \{string} The session id to get tools for | | `options?` | `SchemaModifierOptions` | \{SchemaModifierOptions} Optional configuration for tool retrieval | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The list of session tools **Example** ```typescript const sessionTools = await composio.tools.getRawToolRouterSessionTools('session_123'); console.log(sessionTools); ``` *** ### getToolsEnum() [#gettoolsenum] Fetches the list of all available tools in the Composio SDK. This method is mostly used by the CLI to get the list of tools. No filtering is done on the tools, the list is cached in the backend, no further optimization is required. ```typescript async getToolsEnum(requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` — The complete list of all available tools with their metadata **Example** ```typescript // Get all available tools as an enum const toolsEnum = await composio.tools.getToolsEnum(); console.log(toolsEnum.items); ``` *** ### proxyExecute() [#proxyexecute] Proxies a custom request to a toolkit/integration. This method allows sending custom requests to a specific toolkit or integration when you need more flexibility than the standard tool execution methods provide. ```typescript async proxyExecute(body: ToolProxyParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------------------------------------------- | | `body` | `ToolProxyParams` | The parameters for the proxy request including toolkit slug and custom data | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The response from the proxied request **Example** ```typescript // Send a custom request to a toolkit const response = await composio.tools.proxyExecute({ toolkitSlug: 'github', userId: 'default', data: { endpoint: '/repos/owner/repo/issues', method: 'GET' } }); console.log(response.data); ``` *** --- # Triggers (/reference/sdk-reference/typescript/triggers) ## Usage [#usage] Access this class through the `composio.triggers` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.triggers.list(); ``` ## Methods [#methods] ### create() [#create] Create a new trigger instance for a user If the connected account id is not provided, the first connected account for the user and toolkit will be used ```typescript async create(userId: string, slug: string, body?: TriggerInstanceUpsertParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ----------------------------- | --------------------------------------------- | | `userId` | `string` | The user id of the trigger instance | | `slug` | `string` | The slug of the trigger instance | | `body?` | `TriggerInstanceUpsertParams` | The parameters to create the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The created trigger instance *** ### delete() [#delete] Delete a trigger instance ```typescript async delete(triggerId: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------- | | `triggerId` | `string` | The slug of the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` *** ### disable() [#disable] Disable a trigger instance ```typescript async disable(triggerId: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------ | | `triggerId` | `string` | The id of the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated trigger instance *** ### enable() [#enable] Enable a trigger instance ```typescript async enable(triggerId: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------ | | `triggerId` | `string` | The id of the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated trigger instance *** ### getType() [#gettype] Retrieve a trigger type by its slug for the provided version of the app Use the global toolkit versions param when initializing composio to pass a toolkitversion ```typescript async getType(slug: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ---------------------------- | | `slug` | `string` | The slug of the trigger type | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The trigger type object *** ### listActive() [#listactive] Fetch list of all the active triggers ```typescript async listActive(query?: TriggerInstanceListActiveParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | --------------------------------- | ---------------------------------------------------- | | `query?` | `TriggerInstanceListActiveParams` | The query parameters to filter the trigger instances | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — List of trigger instances **Example** ```typescript const triggers = await triggers.listActive({ authConfigIds: ['123'], connectedAccountIds: ['456'], }); ``` *** ### listEnum() [#listenum] Fetches the list of all the available trigger enums This method is used by the CLI where filters are not required. ```typescript async listEnum(requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ### listTypes() [#listtypes] List all the trigger types ```typescript async listTypes(query?: TriggersTypeListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------ | | `query?` | `TriggersTypeListParams` | The query parameters to filter the trigger types | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The list of trigger types *** ### parse() [#parse] Parse an incoming webhook HTTP request into a typed, normalized trigger payload. Dump the incoming request in and get back the parsed Composio trigger event. When `verifySecret` is provided, the request signature is verified before the payload is returned (delegating to verifyWebhook); without it, the body is parsed without verification. The `request` may be either a Fetch API `Request` (Next.js App Router, Hono, Remix) or a plain `{ body, headers }` object (Express with `express.raw`, Next.js Pages Router `req`). The signature headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`) are read case-insensitively. ```typescript async parse(request: WebhookRequestLike, options?: ParseWebhookOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | --------------------- | --------------------------------- | | `request` | `WebhookRequestLike` | The incoming webhook HTTP request | | `options?` | `ParseWebhookOptions` | Parse options | **Returns** `Promise` — The parsed (and optionally verified) webhook payload **Example** ```typescript // Express with express.raw (verify the signature) app.post('/webhooks/composio', express.raw({ type: 'application/json' }), async (req, res) => { try { const result = await composio.triggers.parse(req, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); console.log('Trigger:', result.payload.triggerSlug); console.log('Event data:', result.payload.payload); res.sendStatus(200); } catch (error) { res.sendStatus(401); } }); // Express without verifying (parse only) app.post('/webhooks/composio', express.raw({ type: 'application/json' }), async (req, res) => { const result = await composio.triggers.parse(req); console.log('Trigger:', result.payload.triggerSlug); res.sendStatus(200); }); ``` ```typescript // Next.js App Router (Request) — verify the signature export async function POST(request: Request) { try { const result = await composio.triggers.parse(request, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); console.log('Trigger:', result.payload.triggerSlug); console.log('Event data:', result.payload.payload); return new Response('OK', { status: 200 }); } catch (error) { return new Response('Unauthorized', { status: 401 }); } } // Next.js App Router — parse only (no verification) export async function POST(request: Request) { const result = await composio.triggers.parse(request); console.log('Trigger:', result.payload.triggerSlug); return new Response('OK', { status: 200 }); } ``` *** ### setWebhookSubscription() [#setwebhooksubscription] Create or update the project webhook subscription used for webhook delivery. If a subscription already exists, the first subscription is updated. Otherwise a new subscription is created. By default this subscribes to V3 trigger message events. ```typescript async setWebhookSubscription(params: SetWebhookSubscriptionParams): Promise ``` **Parameters** | Name | Type | | -------- | ------------------------------ | | `params` | `SetWebhookSubscriptionParams` | **Returns** `Promise` **Example** ```typescript await composio.triggers.setWebhookSubscription({ webhookUrl: `${APP_URL}/webhooks/composio`, }); ``` *** ### subscribe() [#subscribe] Subscribe to all the triggers ```typescript async subscribe(fn: (_data: IncomingTriggerPayload) => void, filters: TriggerSubscribeParams): Promise ``` **Parameters** | Name | Type | Description | | --------- | ----------------------------------------- | ----------------------------------------------- | | `fn` | `(_data: IncomingTriggerPayload) => void` | The function to call when a trigger is received | | `filters` | `TriggerSubscribeParams` | The filters to apply to the triggers | **Returns** `Promise` **Example** ```typescript triggers.subscribe((data) => { console.log(data); }, ); ``` *** ### unsubscribe() [#unsubscribe] Unsubscribe from all the triggers ```typescript async unsubscribe(): Promise ``` **Returns** `Promise` **Example** ```typescript composio.trigger.subscribe((data) => { console.log(data); }); await triggers.unsubscribe(); ``` *** ### update() [#update] Update an existing trigger instance ```typescript async update(triggerId: string, body: TriggerInstanceManageUpdateParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ----------------------------------- | --------------------------------------------- | | `triggerId` | `string` | The Id of the trigger instance | | `body` | `TriggerInstanceManageUpdateParams` | The parameters to update the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated trigger instance response *** ### verifyWebhook() [#verifywebhook] Verify an incoming webhook payload and signature. This method validates that the webhook request is authentic by: 1. Verifying the HMAC-SHA256 signature matches the payload using the correct signing format 2. Optionally checking that the webhook timestamp is within the tolerance window The signature is computed as: `HMAC-SHA256(${webhookId}.${webhookTimestamp}.${payload}, secret)` and is expected in the format: `v1,base64EncodedSignature` ```typescript async verifyWebhook(params: VerifyWebhookParams): Promise ``` **Parameters** | Name | Type | Description | | -------- | --------------------- | --------------------------- | | `params` | `VerifyWebhookParams` | The verification parameters | **Returns** `Promise` — The verified and parsed webhook payload with version information **Example** ```typescript // In an Express.js webhook handler app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { try { const result = await composio.triggers.verifyWebhook({ payload: req.body.toString(), signature: req.headers['webhook-signature'] as string, webhookId: req.headers['webhook-id'] as string, webhookTimestamp: req.headers['webhook-timestamp'] as string, secret: process.env.COMPOSIO_WEBHOOK_SECRET!, }); // Process the verified payload console.log('Webhook version:', result.version); console.log('Received trigger:', result.payload.triggerSlug); res.status(200).send('OK'); } catch (error) { console.error('Webhook verification failed:', error); res.status(401).send('Unauthorized'); } }); ``` *** --- # Toolkits --- # Toolkits (/toolkits) --- # Managed OAuth apps (/toolkits/managed-auth) Composio can provide the OAuth app that your users authorize when they connect a toolkit. You do not need to register an OAuth app or supply its client ID and client secret. Your users still authorize their accounts through a [Connect Link](/docs/tools-direct/authenticating-tools#hosted-authentication-connect-link). Composio stores and refreshes the resulting tokens. See [Authentication](/docs/authentication) for the full connection flow. This page covers only toolkits that support OAuth. It does not list toolkits that use only API keys, bearer tokens, Basic auth, or no authentication. ## When Composio provides the OAuth app [#when-composio-provides-the-oauth-app] For a toolkit with managed OAuth, Composio maintains the OAuth app registration, client credentials, and redirect URI. Your users sign in to the provider and approve the requested permissions. ## When you provide the OAuth app [#when-you-provide-the-oauth-app] Register your own OAuth app when a toolkit does not have managed OAuth. Then create a custom auth config with the app's client ID and client secret. You can also provide your own OAuth app to show your app name on the consent screen, request custom scopes, or use a separate rate-limit quota. See [Managed vs custom auth](/docs/authentication/custom-app-vs-managed-app) for setup steps and trade-offs. ## Find an OAuth toolkit [#find-an-oauth-toolkit] Search for a toolkit to check whether Composio provides a managed OAuth app. * **Composio-managed OAuth available** means that you can use Composio's OAuth app. * **Bring your own OAuth app** means that you must register an OAuth app with the provider and create a custom auth config. Some toolkits support more than one authentication method. A toolkit appears under **Composio-managed OAuth available** when Composio manages at least one OAuth method. Open the toolkit page to check each method. ## Check with the API [#check-with-the-api] Call the toolkit endpoint and read `composio_managed_auth_schemes`: ```bash curl 'https://backend.composio.dev/api/v3.1/toolkits/gmail' \ -H 'x-api-key: YOUR_API_KEY' ``` If `composio_managed_auth_schemes` contains the toolkit's OAuth method, Composio provides the OAuth app. If the field does not contain that method, register your own OAuth app and supply its client ID and client secret. --- # Premium Tools (/toolkits/pro-tools) Some tool calls cost more to run — search APIs, code sandboxes, ML inference. We call those premium tools and price them separately. ## What counts as a premium tool? [#what-counts-as-a-premium-tool] - [Search APIs](/toolkits/composio_search): Composio Search, Perplexity, Exa, SerpAPI - [Code execution](/toolkits/codeinterpreter): Sandboxed runtimes like E2B " title="Web scraping & data extraction" description="Crawlers and structured extraction" /> " title="AI/ML inference" description="Hosted model calls and embeddings" /> " title="Document processing & OCR" description="PDF, image, and document parsing" /> " title="Compute-intensive operations" description="Long-running or heavy transforms" /> ## Pricing [#pricing] Premium tools run on paid third-party providers (web search, media generation, browser automation, and similar). Composio passes the provider's price through with a 5% platform fee — there is no markup on top of that. Approximate per-call prices for each provider are listed in the **Premium tools** section of the [pricing page](https://composio.dev/pricing). * **Hobby** includes up to $2/month of premium tool usage. * **Pro** includes everything in Hobby plus a $29 monthly usage credit, which also covers premium tool calls. * Prices depend on the provider and can change with advance notice — check the [pricing page](https://composio.dev/pricing) for current rates. ## Rate limits [#rate-limits] Premium tools have their own, lower rate limits. These apply to premium tool executions only and are separate from your organization's overall API rate limit — see [Rate Limits](/reference/rate-limits) for that. If you need more, [contact us](mailto:billing@composio.dev). | Plan | Premium Tool Calls Rate Limit | | ---------- | ----------------------------- | | Hobby | 1,000/hour | | Pro | 10,000/hour | | Enterprise | Custom | --- # Get Tool Schemas (/toolkits/meta-tools/get_tool_schemas) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} --- # Meta Tools (/toolkits/meta-tools) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually. To change the intro prose or a tool's one-line summary, edit the template and the override map in lib/meta-tool-overrides.ts. */} Every Composio [session](/docs/how-composio-works) hands your agent a small set of meta tools instead of hundreds of raw tool definitions. The agent uses them to find the right tools for a task, connect the accounts those tools need, execute them, and process the results, all at runtime and all sharing one `session_id`. This keeps your context window small: you load a handful of meta tools, not a catalog of 500+ apps. The agent searches for what it needs when it needs it. A typical workflow runs in order: call `COMPOSIO_SEARCH_TOOLS` to discover tools and open a session, call `COMPOSIO_MANAGE_CONNECTIONS` if a toolkit is not yet connected, then run the tools with `COMPOSIO_MULTI_EXECUTE_TOOL`. Reach for the workbench and bash tools when responses are large enough to process out of context. | Tool | What it does | | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`COMPOSIO_GET_TOOL_SCHEMAS`](/toolkits/meta-tools/get_tool_schemas) | Returns the full input schema for tools you already know the slug for, so you can build schema-compliant arguments before executing | | [`COMPOSIO_MANAGE_CONNECTIONS`](/toolkits/meta-tools/manage_connections) | Checks connection status for a toolkit and returns a branded authentication link when the user needs to connect, covering OAuth, API keys, and every other auth type | | [`COMPOSIO_MULTI_EXECUTE_TOOL`](/toolkits/meta-tools/multi_execute_tool) | Executes up to 50 tools in parallel and returns structured outputs ready for immediate analysis | | [`COMPOSIO_REMOTE_BASH_TOOL`](/toolkits/meta-tools/remote_bash_tool) | Runs bash commands in a remote sandbox for file operations, data processing, and system tasks | | [`COMPOSIO_REMOTE_WORKBENCH`](/toolkits/meta-tools/remote_workbench) | Runs Python in a persistent remote sandbox to process large remote files and script bulk or repeated tool executions | | [`COMPOSIO_SEARCH_TOOLS`](/toolkits/meta-tools/search_tools) | Discovers the right tools across 500+ apps for a task and returns them with an execution plan, connection status, and the `session_id` that ties the rest of the workflow together | > These schemas are for reference only. We do not guarantee backward compatibility for parameter names or response shapes, so do not rely on them as structured type definitions in your code. --- # Manage Connections (/toolkits/meta-tools/manage_connections) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} --- # Multi Execute Tool (/toolkits/meta-tools/multi_execute_tool) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} --- # Remote Bash Tool (/toolkits/meta-tools/remote_bash_tool) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} --- # Remote Workbench (/toolkits/meta-tools/remote_workbench) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} --- # Search Tools (/toolkits/meta-tools/search_tools) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */}