# Shared Connections (Experimental) (/docs/authenticating-users/shared-connections)

> Shared Connections APIs are experimental and may change in future releases. All new fields are nested under an `experimental` block on the wire, and the SDK methods that operate on them live under the `composio.experimental` namespace (`composio.experimental.update_acl` in Python, `composio.experimental.updateAcl` in TypeScript). The code samples below show the experimental shape — pin a specific SDK version if you depend on the current contract.

> If you're building an agent, prefer [sessions](/docs/configuring-sessions). Sessions let you pin a shared connection into a session config; see [Using a shared connection in a session](#using-a-shared-connection) below.

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

Typical use cases:

* **Org-managed credentials** — one Gmail / Salesforce / 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/read mail through your app.

# SHARED vs PRIVATE

|                                    | PRIVATE (default)           | SHARED                                                                                                            |
| ---------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Who can use it**                 | Only the owning `userId`    | The creator + 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 tool-router session, or referenced by `connectedAccountId` on a direct execute |
| **ACL fields**                     | Ignored                     | `allowAllUsers`, `allowedUserIds`, `notAllowedUserIds` (inside the `experimental` block)                          |

> SHARED connections are never used implicitly. A session belonging to a non-creator `userId` will only resolve a SHARED connection if the developer explicitly pins it in the session config.

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

# 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

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

Use the experimental ACL-update method to change access after creation. PATCH semantics — pass only the fields you want to change; omit a field to leave it unchanged; pass an empty array to clear an allow/deny list.

**Python:**

```python
# Open access to everyone.
composio.experimental.update_acl(
    "ca_gmail_shared",
    allow_all_users=True,
)

# Add a targeted allow list (without touching the wildcard or deny list).
composio.experimental.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.experimental.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.experimental.updateAcl("ca_gmail_shared", {
  allowAllUsers: true,
});

// Add a targeted allow list (without touching the wildcard or deny list).
await composio.experimental.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.experimental.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 receive a permission error.

# Using a shared connection

## In a tool-router session

Pin the SHARED connection into the session via `connectedAccounts`. The session belongs to a *different* `userId` than the creator — the pin is what makes the SHARED connection visible to that session.

The session config itself is **not** experimental — only the connection that you're pinning is. So you pin by ID exactly as you would a PRIVATE connection.

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

## Direct execution

Pass the SHARED connection's ID explicitly via `connectedAccountId`:

**Python:**

```python
result = composio.tools.execute(
    slug="GMAIL_SEND_EMAIL",
    user_id="user_alice",
    connected_account_id="ca_gmail_shared",
    arguments={"to": "person@example.com", "subject": "hi", "body": "..."},
)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const result = await composio.tools.execute("GMAIL_SEND_EMAIL", {
  userId: "user_alice",
  connectedAccountId: "ca_gmail_shared",
  arguments: { to: "person@example.com", subject: "hi", body: "..." },
});
```

If `user_alice` isn't permitted by the connection's ACL, the call raises `ComposioSharedAccessDeniedError`.

# Listing SHARED connections

By default `list()` returns **PRIVATE only** — shared accounts must be requested explicitly. Pass an `account_type` (Python) / `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/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

`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                                              | 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 tool-router 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. |

**Python:**

```python
from composio import exceptions

try:
    composio.experimental.update_acl(
        "ca_private_gmail",  # this connection is PRIVATE
        allow_all_users=True,
    )
except exceptions.ComposioAclOnlyForSharedError as e:
    print(f"ACL only applies to SHARED connections: {e}")

try:
    composio.tools.execute(
        slug="GMAIL_SEND_EMAIL",
        user_id="user_bob",  # not on the allow list
        connected_account_id="ca_gmail_shared",
        arguments={...},
    )
except exceptions.ComposioSharedAccessDeniedError as e:
    print(f"Access denied: {e}")
```

**TypeScript:**

```typescript
import {
  Composio,
  ComposioAclOnlyForSharedError,
  ComposioSharedAccessDeniedError,
} from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
try {
  await composio.experimental.updateAcl("ca_private_gmail", {
    allowAllUsers: true,
  });
} catch (error) {
  if (error instanceof ComposioAclOnlyForSharedError) {
    console.log(`ACL only applies to SHARED connections: ${error.message}`);
  }
}

try {
  await composio.tools.execute("GMAIL_SEND_EMAIL", {
    userId: "user_bob",  // not on the allow list
    connectedAccountId: "ca_gmail_shared",
    arguments: { /* ... */ },
  });
} catch (error) {
  if (error instanceof ComposioSharedAccessDeniedError) {
    console.log(`Access denied: ${error.message}`);
  }
}
```

# What to read next

- [Connected accounts](/docs/auth-configuration/connected-accounts): Manage and monitor user connections to toolkits

- [Managing multiple connected accounts](/docs/managing-multiple-connected-accounts): Pin specific accounts into a session at runtime

- [Configuring sessions](/docs/configuring-sessions): Session config — custom auth configs, pinned connections, multi-account

---

📚 **More documentation:** [View all docs](https://docs.composio.dev/llms.txt) | [Glossary](https://docs.composio.dev/llms.mdx/docs/glossary) | [Cookbooks](https://docs.composio.dev/llms.mdx/cookbooks) | [API Reference](https://docs.composio.dev/llms.mdx/reference)

---

# Composio SDK — Instructions for AI Code Generators

**Context:** This page documents the **direct tool execution API**. For most applications, the session-based pattern (`composio.create(user_id)` + `session.tools()`) is simpler — but if the user is here, they likely want direct execution.

---

## Key Patterns

### Authenticating Users

Create an **Auth Config** on [platform.composio.dev](https://platform.composio.dev), then use the auth config ID to generate a hosted auth URL:

```python
from composio import Composio

composio = Composio()
connection = composio.connected_accounts.link(
    user_id="user_123",
    auth_config_id="ac_...",  # from platform dashboard
)
print(connection.redirect_url)  # send user here to authenticate
```

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

const composio = new Composio();
const connection = await composio.connectedAccounts.link("user_123", "ac_...", {
    callbackUrl: "https://your-app.com/callback",
});
console.log(connection.redirectUrl); // send user here to authenticate
```

### Executing Tools

```python
tools = composio.tools.get("user_123", tools=["GITHUB_CREATE_ISSUE"])

result = composio.tools.execute(
    "GITHUB_CREATE_ISSUE",
    {"owner": "org", "repo": "repo", "title": "Bug report"},
    user_id="user_123",
)
```

```typescript
const tools = await composio.tools.get("user_123", { tools: ["GITHUB_CREATE_ISSUE"] });

const result = await composio.tools.execute("GITHUB_CREATE_ISSUE", {
    userId: "user_123",
    arguments: { owner: "org", repo: "repo", title: "Bug report" },
});
```

---

## Rules

1. **`user_id` is required** — pass it to `tools.get()`, `tools.execute()`, and `provider.handle_tool_calls()`.
2. **`tools.execute()` signature** — Python: `execute(slug, arguments_dict, *, user_id=...)` (arguments is the second positional param). TypeScript: `execute(slug, { userId, arguments })`.
3. **Provider at init** — `Composio(provider=OpenAIProvider())` in Python, `new Composio({ provider: new OpenAIProvider() })` in TypeScript. Defaults to OpenAI if omitted.
4. **Correct provider imports** — `composio_<provider>` for Python, `@composio/<provider>` for TypeScript. For OpenAI Agents SDK use `composio_openai_agents` / `@composio/openai-agents`.


---

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

