Authenticating users

Shared Connections (Experimental)

Markdown

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. Sessions let you pin a shared connection into a session config; see Using a shared connection in a session 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 userIds, 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 userIds.
  • Team mailboxessupport@ or sales@ accounts where any teammate can send/read mail through your app.

SHARED vs PRIVATE

PRIVATE (default)SHARED
Who can use itOnly the owning userIdThe creator + 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 tool-router session, or referenced by connectedAccountId on a direct execute
ACL fieldsIgnoredallowAllUsers, 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).

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

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

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.

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

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

Direct execution

Pass the SHARED connection's ID explicitly via connectedAccountId:

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": "..."},
)
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.

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/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 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.
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}")
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}`);
  }
}