# Connect a customer’s accounts

Let customers connect an app, choose understandable permissions, and change or remove access from your interface. This belongs to the optional **Customer agents integration path**. It reuses core named connections, tool ceilings, project/preset rules and dispatch-time checks; it does not create another permission system.

## Before offering Connect

1. Implement [authenticated customer setup](https://staging.macrofold.ai/docs/raw/customer-agents/quickstart.md). All Macrofold SDK calls happen on your server, using the same owning member and organization as the binding.
2. The deployment operator enables the toolkit, pins its version and configures the provider’s verified HTTPS callback. Follow [connector setup](https://github.com/Macrofold/Macrofold/blob/main/docs/features/identity-integrations/composio.md). Cloud users need an enabled connector; self-hosted operators configure it themselves. A toolkit in the directory is not necessarily enabled.
3. Review exact tools into a small set of capability choices. For example, “Read my calendar” and “Create events” should be separate. Expand **View permissions** for exact tool names. Do not infer safe tools from name prefixes or provide an unreviewed “all tools” permission.
4. Configure a fixed HTTPS callback in **your app’s backend**, with normal app authentication and a server-stored random state. Never accept a return URL from untrusted browser input.

A customer does not need a Macrofold login. Their app session and upstream account consent are both checked before activation. Provider OAuth scopes may be broader than the agent’s exact tool allowlist; explain both boundaries to the customer.

## 1. Prepare a named connection

After resolving `customerId` from the app session and `assistantId` from its stored binding:

```ts
const account = await client.customerAgents.createConnection(
  customerId,
  assistantId,
  {
    name: 'My GitHub profile',
    provider: 'github',
    capabilities: [
      {
        id: 'profile',
        label: 'Read my profile',
        description: 'Read your signed-in GitHub profile. Does not grant repository access.',
        tools: ['GITHUB_GET_THE_AUTHENTICATED_USER'],
      },
    ],
  },
  { idempotencyKey: connectionActionKey },
);
```

This example requires the enabled GitHub toolkit to contain that exact tool. Creation validates every tool against the enabled version and starts with **No access**. Save `account.connection.id` in your app; retry creation only with the same stored key and body. Capabilities are your reviewed server configuration, not text generated by the agent or submitted by a customer. Use separate named connections for different upstream accounts.

The response includes capabilities, selected capability IDs, approved tool names and an `access_version` string. The capability configuration is creation-only; a changed product capability set requires a new reviewed connection and consent.

## 2. Start consent from your backend

```ts
const link = await client.customerAgents.authorizeConnection(
  customerId,
  assistantId,
  connectionId,
  {
    return_url: appCallbackWithStoredState,
  },
  { idempotencyKey: authorizationActionKey },
);
// Send only this short-lived URL to the authenticated customer’s browser.
```

`appCallbackWithStoredState` is your fixed backend callback plus a random `state` query parameter. Store state → customer ID, assistant ID, connection ID, authorization ID and expiry in your database. Bind it to the app session, and reject unknown, expired or wrong-customer state. Keep it available until completion is confirmed so network retries are recoverable.

Navigate to `link.authorization_url`. The hosted Macrofold page explains permissions, starts with No access for a new account, and shows the destination app. The customer selects capabilities and signs in to the provider. The link expires after ten minutes. Its ticket is in the URL fragment, not a server query string; treat the whole link as sensitive.

The verified provider callback returns the customer to your backend with your original `state` and a single-use `connection_code`. **That redirect alone does not activate access.** A copied consent link must not let a different app user attach an account.

## 3. Complete only after app authentication

Your callback must perform these steps in order:

1. Authenticate the current app user. If signed out, complete your normal sign-in and resume the pending callback without losing state. Do not trust a customer ID in the URL.
2. Load the stored state and compare its customer and session with that verified user. Resolve the binding and connection from your stored record, never from callback-supplied IDs.
3. Call the completion operation with the returned code and a durable completion idempotency key:

```ts
const connected = await client.customerAgents.completeConnection(
  customerId,
  assistantId,
  connectionId,
  {
    code: connectionCode,
  },
  { idempotencyKey: completionActionKey },
);
```

4. Mark your callback state completed only after a confirmed response. Redirect to a clean app URL and reload the connections list. Do not render or log the code. Use `Cache-Control: no-store` and `Referrer-Policy: no-referrer` on your callback response; redact callback query strings from access logs.

Macrofold verifies the provider session against a customer-specific opaque subject, checks the exact account/toolkit and current authority, then grants only the selected tools to this binding’s exact project + preset. Starting later runs through this path explicitly selects the linked, healthy, approved accounts. Another customer’s binding or connection ID returns 404.

## 4. Embed the optional React controls

Install the [TypeScript SDK](https://staging.macrofold.ai/docs/raw/sdk/typescript.md) and React 19 in your app. These components are presentation-only and work with callbacks to your authenticated backend; they do not accept platform keys.

```tsx
import { CustomerConnectionCard } from 'macrofold/react';
import 'macrofold/react.css'; // Optional baseline styles; override in your design system.

<CustomerConnectionCard
  key={account.connection.id}
  value={account}
  onAuthorize={openConsentThroughYourBackend}
  onSave={savePermissionsThroughYourBackend}
  onDisconnect={disconnectThroughYourBackend}
/>;
```

`value` is one entry from `customerAgents.listConnections`. `onAuthorize()` asks your backend for a new link and navigates to it. `onSave(capabilityIds, version)` calls your backend, which reauthenticates and updates permissions below. `onDisconnect()` calls your backend and removes the card after success. Callbacks return promises and must throw on failure; refresh `value` after successful mutations. The component shows errors, disables pending actions, confirms disconnect and requires reloading a stale permission draft. `ConnectionPermissions` is also exported as a controlled fieldset if you want to build your own card.

```ts
const updated = await client.customerAgents.updateConnectionPermissions(
  customerId,
  assistantId,
  connectionId,
  {
    capability_ids: selectedCapabilityIds,
    ifMatch: `"${accessVersion}"`,
  },
  { idempotencyKey: permissionActionKey },
);
```

An empty array means No access. A stale version returns 412: reload and let the customer review the change; do not automatically resubmit an old selection against a fresh version. New permissions require a healthy verified account. Narrowing permissions uses the existing runtime revocation checks; it cannot reverse a completed external side effect.

Reconnect uses `authorizeConnection` again and preserves the exact provider account ID. If the provider cannot reconnect that account, explicitly disconnect and create a new named connection. Never silently switch accounts. Disconnect uses `deleteConnection`; the existing cleanup boundary removes agent access and schedules upstream cleanup. The app should remove the disconnected card after confirmation.

## Recovery and important limits

| Response or state                      | Action                                                                                                                                                           |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 404                                    | Re-resolve the authenticated customer and stored IDs; do not fall back to another customer or core resource.                                                     |
| Expired/replaced link                  | Request a fresh authorization link.                                                                                                                              |
| Permission version conflict (412)      | Reload current permissions and review them; old consent cannot restore revoked access.                                                                           |
| `connection_recovery_required`         | Account creation may have reached the provider. Ask the operator to reconcile the recorded attempt before retrying.                                              |
| Provider verification uncertain/failed | Reconnect the same named account with a new link; do not replay a consumed provider URI.                                                                         |
| SDK response lost                      | Retry the identical request with the original idempotency key. A stored verified receipt prevents redeeming the provider URI twice after a local commit failure. |
| Owning member/key revoked              | Issue a new link under an authorized credential belonging to the same owner; an old link is no longer authority.                                                 |

Connections currently use enabled hosted app toolkits. This path does not embed arbitrary MCP OAuth, collect raw provider keys, or introduce customer OAuth tokens for the Macrofold API. Use the core [connection API](https://staging.macrofold.ai/docs/raw/connections.md) for other supported methods. One browser can have one active provider consent journey at a time; starting a dashboard connection replaces its customer-consent cookie and vice versa.

Before rollout, test real HTTPS callbacks, selected provider scopes, reconnect and revocation on your deployment. Deterministic local fixtures exercise the boundaries but do not prove upstream OAuth configuration or real account behavior.
