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

# App integrations

> Connect Gmail, Slack, Notion, and 250+ other apps to an instance over the Hosting API: managed Composio, one entity per instance, OAuth handled for you.

Every instance ships with managed [Composio](https://composio.dev) credentials, so the agent can call real apps (Gmail, Slack, Notion, GitHub, Google Calendar, and hundreds more) without you wiring up OAuth or holding any provider tokens. The agent can connect apps in conversation, but you can also drive the whole flow over the Hosting API: browse the catalog, start a connection, list what's connected, and disconnect.

These endpoints live under each instance and operate on that instance's own Composio entity.

Prefer to run integrations on **your own** Composio project, with your API key, your users' connected accounts, and your own metering? That's a different path: see [Use your own Composio](/docs/agents-api/composio).

<Info>
  Browsing, connecting, listing, and disconnecting are **free**: they never debit the wallet or the budget. Only the agent's actual tool calls at runtime are metered, as managed Composio spend. See [Billing](#billing) below for the rate.
</Info>

## The entity model

Each instance maps to exactly one Composio entity, derived from your workspace and the instance id. You never construct or pass it; every endpoint here resolves it from the path instance. The practical consequences:

* **Per-instance isolation.** Create one instance per end user (as in [Instances](/docs/agents-api/instances)) and their connected accounts never leak across users.
* **Multiple accounts of the same app.** Connect the same toolkit more than once to attach, say, two Gmail accounts to one instance. Each call returns a distinct `connectedAccountId`; the agent chooses between them per tool call by `connected_account_id`.
* **Survives restarts and rolls.** Connections belong to the entity, not the running container, so they persist across stop/start and image updates.

## Endpoints

| Method   | Path                                                               | Returns                        |
| -------- | ------------------------------------------------------------------ | ------------------------------ |
| `GET`    | `/v1/instances/{id}/integrations/toolkits`                         | `200` a page of available apps |
| `POST`   | `/v1/instances/{id}/integrations/connect`                          | `200` an authorization link    |
| `GET`    | `/v1/instances/{id}/integrations/connections`                      | `200` connected accounts       |
| `DELETE` | `/v1/instances/{id}/integrations/connections/{connectedAccountId}` | `200` deletion confirmation    |

All four require the `sk_live_` key, and the path instance must belong to your workspace; otherwise the call returns `404`.

## Browse the app catalog

`GET /v1/instances/{id}/integrations/toolkits` lists the apps you can connect, newest-relevant first, paginated by cursor.

<ParamField query="search" type="string">
  Filter the catalog by name or slug. Must be at least 3 characters; a shorter value returns `400`.
</ParamField>

<ParamField query="limit" type="integer" default="12">
  Page size, clamped to `1`–`24`.
</ParamField>

<ParamField query="cursor" type="string">
  The `nextCursor` from a previous page. Omit for the first page.
</ParamField>

<ResponseField name="items" type="array">
  The page of toolkits. Each carries `slug`, `name`, `description`, `logo`, `enabled`, `isNoAuth`, and `authSchemes`.
</ResponseField>

<ResponseField name="nextCursor" type="string | null">
  Pass back as `cursor` to fetch the next page. `null` on the last page.
</ResponseField>

<ResponseField name="totalItems" type="integer">
  Total matches for the query.
</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/toolkits?search=gmail&limit=5" \
    -H "Authorization: Bearer sk_live_..."
  ```

  ```python python theme={null}
  import requests

  H = {"Authorization": "Bearer sk_live_..."}
  page = requests.get(
      "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/toolkits",
      headers=H,
      params={"search": "gmail", "limit": 5},
  ).json()
  ```

  ```javascript node theme={null}
  const H = { Authorization: "Bearer sk_live_..." };
  const page = await (await fetch(
    "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/toolkits?search=gmail&limit=5",
    { headers: H },
  )).json();
  ```

  ```json response theme={null}
  {
    "items": [
      {
        "slug": "gmail",
        "name": "Gmail",
        "description": "Send, read, and search email.",
        "logo": "https://logos.composio.dev/api/gmail",
        "enabled": true,
        "isNoAuth": false,
        "authSchemes": ["OAUTH2"]
      }
    ],
    "nextCursor": null,
    "totalItems": 1
  }
  ```
</CodeGroup>

## Connect an app

`POST /v1/instances/{id}/integrations/connect` starts an OAuth connection for one toolkit and returns an authorization link. Open `redirectUrl` in a browser, grant access, and the connection becomes active for this instance's entity.

<ParamField body="toolkit" type="string" required>
  The toolkit slug to connect, for example `gmail` (from the catalog's `slug`).
</ParamField>

<ParamField body="callbackUrl" type="string">
  Where to send the user after they grant access. If present it **must** be an absolute `https://` URL (anything else returns `400`). Omit it to use Composio's hosted "you can close this window" page, with no callback needed.
</ParamField>

The workspace's own Composio auth configs are applied automatically, so toolkits you've set up with custom OAuth credentials use them transparently.

<ResponseField name="toolkit" type="string">
  The toolkit slug, echoed back.
</ResponseField>

<ResponseField name="connectedAccountId" type="string">
  The id of the pending connected account. Becomes active once the user completes the link, and identifies this account in [connections](#list-connections) and at tool-call time.
</ResponseField>

<ResponseField name="redirectUrl" type="string">
  The authorization URL to open in a browser.
</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connect \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{ "toolkit": "gmail" }'
  ```

  ```python python theme={null}
  conn = requests.post(
      "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connect",
      headers={**H, "Content-Type": "application/json"},
      json={"toolkit": "gmail"},
  ).json()
  # Send the user to conn["redirectUrl"].
  ```

  ```javascript node theme={null}
  const conn = await (await fetch(
    "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connect",
    {
      method: "POST",
      headers: { ...H, "Content-Type": "application/json" },
      body: JSON.stringify({ toolkit: "gmail" }),
    },
  )).json();
  ```

  ```json response theme={null}
  {
    "toolkit": "gmail",
    "connectedAccountId": "ca_7f3a9b21",
    "redirectUrl": "https://backend.composio.dev/api/v3/.../authorize"
  }
  ```
</CodeGroup>

Pass a `callbackUrl` to return the user to your own app after they grant access:

```json theme={null}
{ "toolkit": "gmail", "callbackUrl": "https://app.example.com/integrations/done" }
```

### Returning the user to your app

`callbackUrl` is where Composio sends the user once they grant access. Point it at a page in your own app to keep the flow seamless instead of leaving them on Composio's hosted page. Two things make the round-trip clean:

* **Carry your own context.** Add query params to the `callbackUrl` you pass, so the page they land on knows what just happened, for example `https://app.example.com/integrations/done?toolkit=gmail`. The user returns to your URL with those params intact; Composio doesn't add any of its own, so put everything you need to know there yourself.
* **Confirm it took.** Landing back on your page means the user finished the OAuth screens, not that the account is live. Verify by calling [`GET /v1/instances/{id}/integrations/connections`](#list-connections): match the `connectedAccountId` you got from `connect` and check its `status` is `ACTIVE`. A new connection can take a moment to settle, so poll briefly if it isn't active on the first read.

### Multiple accounts of one app

Call `connect` again for the same toolkit to add a second account, say a second Gmail inbox. You get a fresh `connectedAccountId`, and both stay attached to the instance. The agent picks which to use per tool call via `connected_account_id`, so "send from my work address" and "send from my personal address" both work on one instance.

### When a toolkit needs your own credentials

Some toolkits have no managed OAuth app and require your own credentials. For those, `connect` returns `422`:

```json theme={null}
{ "error": "custom_auth_required", "toolkit": "salesforce" }
```

Configure the toolkit's auth config for your workspace, then retry. If managed Composio isn't configured at all (or is temporarily unavailable), the call returns `503`.

## List connections

`GET /v1/instances/{id}/integrations/connections` returns the accounts connected to this instance. Pass `toolkit` to filter to one app.

<ParamField query="toolkit" type="string">
  Return only connections for this toolkit slug.
</ParamField>

<ResponseField name="connections" type="array">
  Composio connected-account objects, passed through as-is. Each carries `id`, `toolkitSlug`, `toolkitName`, `status`, and the account's auth and timestamp metadata. Because this is Composio's native shape, its fields follow Composio's naming and its timestamps are epoch **milliseconds** (not the Hosting API's usual seconds).
</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connections?toolkit=gmail" \
    -H "Authorization: Bearer sk_live_..."
  ```

  ```python python theme={null}
  conns = requests.get(
      "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connections",
      headers=H,
      params={"toolkit": "gmail"},
  ).json()
  ```

  ```javascript node theme={null}
  const conns = await (await fetch(
    "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connections?toolkit=gmail",
    { headers: H },
  )).json();
  ```

  ```json response theme={null}
  {
    "connections": [
      {
        "id": "ca_7f3a9b21",
        "toolkitSlug": "gmail",
        "toolkitName": "Gmail",
        "status": "ACTIVE",
        "authConfigId": "ac_managed_gmail",
        "authScheme": "OAUTH2",
        "isDisabled": false,
        "createdAt": 1781222400000,
        "updatedAt": 1781222480000
      }
    ]
  }
  ```
</CodeGroup>

## Disconnect an app

`DELETE /v1/instances/{id}/integrations/connections/{connectedAccountId}` removes one connected account. The account must belong to this instance's entity, or the call returns `404`. After deletion the agent can no longer use that account; other accounts on the instance are untouched.

<CodeGroup>
  ```bash curl theme={null}
  curl -X DELETE \
    https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connections/ca_7f3a9b21 \
    -H "Authorization: Bearer sk_live_..."
  ```

  ```python python theme={null}
  res = requests.delete(
      "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connections/ca_7f3a9b21",
      headers=H,
  ).json()
  ```

  ```javascript node theme={null}
  const res = await (await fetch(
    "https://api.agent37.com/v1/instances/ab12cd34ef/integrations/connections/ca_7f3a9b21",
    { method: "DELETE", headers: H },
  )).json();
  ```

  ```json response theme={null}
  { "id": "ca_7f3a9b21", "deleted": true }
  ```
</CodeGroup>

## Billing

Managing integrations is free: nothing on this page debits the budget or the wallet. Only the agent actually calling a connected app at runtime is metered, at \$0.000114 per call (114 micros), drawn from the instance [budget](/docs/agents-api/budgets) and the workspace wallet like any other managed service.

That spend shows up in `GET /v1/instances/{id}/usage` under `by_integration.composio`. See [Managed services & budgets](/docs/agents-api/budgets) for the rate, the usage shape, and the `402` refusal that keeps the instance running when the budget or wallet can't cover a tool call.
