> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runtools.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Hosted Connect

> Let users of your Runtools-built products connect their own integrations

Hosted Connect lets a product built on Runtools ask its own users to connect their own third-party accounts. End users do not create Runtools accounts. Your product keeps its own auth system, passes a stable `externalUserId`, and Runtools stores credentials scoped to:

```text theme={null}
builder org + appId + externalUserId + tool
```

Use this when you build an agent product like Atlas and each customer needs to connect their own Stripe, Gmail, Sheets, Slack, or API-key-backed tools.

<Warning>
  Create connect sessions from your server. Keep your Runtools API key server-side and only send the returned `connectUrl` to the browser.
</Warning>

## Hosted Connect vs. Connected Apps

| Surface                           | Owner                              | Used for                                                                 |
| --------------------------------- | ---------------------------------- | ------------------------------------------------------------------------ |
| [Connected Apps](/advanced/oauth) | Your Runtools user or organization | Internal dashboard tools and agents that use your own connected accounts |
| Hosted Connect                    | Your product's end users           | Customer-owned accounts for products you build on Runtools               |

Connections are app-scoped by default. A Gmail connection for `atlas` does not automatically become a Gmail connection for `iris` or any other product.

## Create a product app

Configure one app per product. The `appId` is your stable product identifier, such as `atlas`.

```typescript theme={null}
import { RunTools } from '@runtools-ai/sdk';

const rt = new RunTools({
  apiKey: process.env.RUNTOOLS_API_KEY,
});

await rt.connect.upsertApp({
  appId: 'atlas',
  slug: 'atlas',
  name: 'Atlas',
  allowedRedirectUrls: [
    'https://atlas.example.com/settings/integrations',
  ],
  allowedToolSlugs: [
    'stripe',
    'quickbooks',
    'xero',
    'google-sheets',
    'gmail',
    'slack',
    'hubspot',
  ],
  oauthAppPolicy: 'auto',
});
```

`oauthAppPolicy` controls which OAuth client is used:

| Policy                  | Behavior                                                                                              |
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
| `auto`                  | Use an app-specific custom OAuth app, then an org default, then Runtools-managed OAuth when available |
| `custom_required`       | Require the builder to configure a custom OAuth app for this product                                  |
| `runtools_managed_only` | Always use the Runtools-managed OAuth app when available                                              |

## Create a connect session

When a signed-in product user clicks "Connect integrations", create a short-lived hosted connect session from your server.

```typescript theme={null}
const session = await rt.connect.createSession({
  appId: 'atlas',
  externalUserId: currentUser.id,
  email: currentUser.email,
  name: currentUser.name,
  integrations: ['stripe', 'google-sheets', 'gmail'],
  returnUrl: 'https://atlas.example.com/settings/integrations',
  successUrl: 'https://atlas.example.com/settings/integrations?connected=1',
});

return Response.json({
  connectUrl: session.connectUrl,
});
```

Open `session.connectUrl` in the browser. The hosted page shows the requested integrations, starts OAuth where needed, and collects API-key-style credentials for tools that use secrets instead of OAuth.

```typescript theme={null}
window.location.href = connectUrl;
```

## Run an agent with that user's connections

When your product runs an agent, pass the same app and user scope.

```typescript theme={null}
const run = await rt.agent.run(
  'atlas-agent',
  'Summarize revenue, cash, runway, and finance risks for this month.',
  {
    endUser: {
      appId: 'atlas',
      externalUserId: currentUser.id,
    },
  },
);
```

Tool execution resolves credentials only from that end user's app-scoped connections. If the required connection is missing, the run returns `connect_required` instead of falling back to builder or org credentials.

Agent-runtime tool calls are capability-scoped internally. The model, sandbox, and managed runtime can request a tool action, but they cannot choose `credentialScope`, `endUser`, raw credentials, or org secret overrides. Runtools derives that authority from the agent run and Hosted Connect app/user scope.

## Use Hosted Connect from channels

For Telegram, WhatsApp, Slack, Discord, iMessage, or SMS products, the developer connects the channel once and end users only chat with it. The channel credential is the transport, not the customer data identity.

Bind the channel to your product app, then link each provider sender id to the same `appId + externalUserId` you use for Hosted Connect:

```typescript theme={null}
await rt.channels.bindProductChannel('atlas', {
  agentChannelId: channel.id,
  identityPolicy: 'link_required',
  groupPolicy: 'sender_scoped',
});

await rt.channels.linkProductChannelUser({
  appId: 'atlas',
  agentChannelId: channel.id,
  externalUserId: telegramUserId,
  productExternalUserId: currentUser.id,
});
```

When the linked sender messages the channel, Runtools starts the agent with:

```typescript theme={null}
{
  endUser: {
    appId: 'atlas',
    externalUserId: currentUser.id,
  }
}
```

Missing product-user connections still return `connect_required`. The channel worker does not fall back to builder Connected Apps, org defaults, or another product user's credentials.

## Run tools directly

You can also execute Tool Hub actions with end-user scope.

```typescript theme={null}
const result = await rt.tools.execute('stripe', {
  action: 'list_customers',
  params: { limit: 10 },
  credentialScope: 'end_user',
  endUser: {
    appId: 'atlas',
    externalUserId: currentUser.id,
  },
});
```

When `endUser` is present, RunTools enforces `credentialScope: 'end_user'` even if you omit it. Passing it explicitly makes your intent clear.

If the user connected multiple accounts for the same tool, pass a connection selector.

```typescript theme={null}
const { connections } = await rt.connect.listConnections({
  appId: 'atlas',
  externalUserId: currentUser.id,
});

await rt.tools.execute('gmail', {
  action: 'list_emails',
  params: { query: 'is:unread' },
  credentialScope: 'end_user',
  endUser: {
    appId: 'atlas',
    externalUserId: currentUser.id,
    connectionIds: connections
      .filter((connection) => connection.toolSlug === 'gmail')
      .map((connection) => connection.id),
  },
});
```

## List connections

Show connection status in your product's settings page.

```typescript theme={null}
const { connections } = await rt.connect.listConnections({
  appId: 'atlas',
  externalUserId: currentUser.id,
});
```

Each connection includes the tool slug, provider, account label, status, default flag, and timestamps. Raw tokens and secret values are never returned.

## API-key-backed tools

Some tools use API keys or secrets instead of OAuth. The hosted connect page can collect those values and store them through the tools service. If you build your own connect UI, post the values through the connect session instead of storing them in your product database.

```typescript theme={null}
await rt.connect.storeSecretCredentials(session.id, {
  toolSlug: 'posthog',
  accountLabel: 'Production Project',
  credentials: {
    POSTHOG_API_KEY: userSubmittedKey,
  },
});
```

The credential is write-only to the product and encrypted by Runtools.

## Usage and audit

Usage is attributed by app and end user so builders can report, rebill, and debug without exposing credentials.

```typescript theme={null}
const usage = await rt.connect.getAppUsage('atlas', { days: 30 });

const audit = await rt.connect.listAuditEvents({
  appId: 'atlas',
  externalUserId: currentUser.id,
  limit: 50,
});
```

Audit events include connection IDs, provider/tool labels, app IDs, external user IDs, event types, and timestamps. They never include raw credential values.

## Custom OAuth apps

By default, Hosted Connect can use Runtools-managed OAuth. If you want the provider consent screen to show your own app name, configure a custom OAuth app.

```typescript theme={null}
await rt.auth.setProviderConfig('google', {
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  scopes: 'openid,email,https://www.googleapis.com/auth/gmail.readonly',
  appId: 'atlas',
});
```

You can configure a provider once for the organization, or override it for a single app by passing `appId`. Hosted Connect resolves app-specific config first, then org-level config, then Runtools-managed OAuth when the app policy allows it.

## REST endpoints

The SDK uses the Auth and Tools service URLs internally. If you call REST directly, use these endpoints from your server:

| Method | URL                                                                                   | Description                                               |
| ------ | ------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `POST` | `https://auth.runtools.ai/v1/connect/apps`                                            | Create or update a product app                            |
| `POST` | `https://auth.runtools.ai/v1/connect/sessions`                                        | Create a hosted connect session                           |
| `GET`  | `https://auth.runtools.ai/v1/connect/sessions/{sessionId}`                            | Read public connect session metadata                      |
| `GET`  | `https://auth.runtools.ai/v1/connect/connections?appId=atlas&externalUserId=user_123` | List one end user's product-scoped connections            |
| `GET`  | `https://auth.runtools.ai/v1/connect/apps/{appId}/usage`                              | Read app-scoped usage                                     |
| `GET`  | `https://auth.runtools.ai/v1/connect/audit-events`                                    | Export connection audit events                            |
| `POST` | `https://tools.runtools.ai/v1/connect/sessions/{sessionId}/secrets`                   | Store API-key-style credentials through a connect session |

Authenticated Auth service calls accept the same Runtools API key as a bearer token:

```bash theme={null}
curl -X POST https://auth.runtools.ai/v1/connect/sessions \
  -H "Authorization: Bearer $RUNTOOLS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "appId": "atlas",
    "externalUserId": "user_123",
    "integrations": ["stripe", "gmail"],
    "returnUrl": "https://atlas.example.com/settings/integrations"
  }'
```

## Production checklist

* Use a stable `externalUserId` from your product, not an email address that can change.
* Create sessions server-side only.
* Keep `allowedRedirectUrls` strict.
* Keep `allowedToolSlugs` limited to tools your product actually needs.
* Never use builder credentials for customer-data tool calls.
* Treat missing end-user credentials as a connect-required state in your product UI.
* Show connection status and reconnect actions inside your product settings.
