Experimental

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 userIDs, subject to a per-connection access control list (ACL).

You use a shared connection by pinning it into a session: 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 userIDs.
  • Team mailboxes. support@ or sales@ accounts where any teammate can send and read mail through your app.

SHARED vs PRIVATE

PRIVATE (default)SHARED
Who can use itOnly the owning userIDThe creator plus every userID permitted by the ACL
Default access for other usersAlways deniedDeny-by-default (the creator must grant access explicitly)
How it's usedImplicit lookup by userIDMust be explicitly pinned in a session
ACL fieldsIgnoredallowAllUsers, allowedUserIds, notAllowedUserIds (inside the experimental block)

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

# 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}")
// 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

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.

# 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()
// 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

When a non-creator userID attempts to use a SHARED connection, the ACL is evaluated in this order:

  1. userIdnotAllowedUserIdsDENY
  2. allowAllUsers === trueALLOW
  3. userIdallowedUserIdsALLOW
  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 userIDs.

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

GoalACL 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

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.

# 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=[],
)
// 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

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.

ValueReturns
'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.

# 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"],
)
// 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.

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

ErrorWhen
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

Configuring sessions

Pin connected accounts, auth configs, and toolkit restrictions into a session