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

# App Connections

> Connect Google Calendar, Sheets, Slack, HubSpot, Salesforce, and Cal.com, and control which tools each agent may use.

A **provider connection** links a third-party account to your org and
exposes a curated set of **agent tools** (e.g. "create a calendar
event", "post to Slack"). You control capability at two levels:

* **Connection-level `allowed_operations`** — which operation groups
  the whole connection permits (e.g. Slack `post: false`).
* **Per-agent attachments** — which agents may use the connection, and
  optionally a narrower per-agent operation override.

OAuth-based providers are connected from the dashboard (the browser
OAuth consent flow can't be driven by an API key). Once connected,
everything on this page is manageable via the API. Cal.com uses an API
key instead and can be connected [directly](#connect-with-an-api-key).

<Note>
  Authorization here is **operation-level, not record-level** — see
  [Authorization model](#authorization-model) below before assuming an
  agent's access can be scoped to specific channels, contacts, or
  calendars.
</Note>

## Providers and their tools

| Provider        | `provider`        | Auth    | Operation groups (defaults)                                              | Agent tools                                                                                                                      |
| --------------- | ----------------- | ------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| Google Calendar | `google_calendar` | OAuth   | `view_events` ✓, `create_events` ✓, `update_events` ✗, `delete_events` ✗ | `calendar_list_events`, `calendar_check_availability`, `calendar_create_event`, `calendar_update_event`, `calendar_cancel_event` |
| Google Sheets   | `google_sheets`   | OAuth   | `append_rows` ✓                                                          | `sheets_append_row` (requires [sheet configuration](#configure-the-target-google-sheet))                                         |
| Slack           | `slack`           | OAuth   | `view` ✓, `post` ✗, `search` ✓                                           | `slack_list_channels`, `slack_post_message`, `slack_search_messages`                                                             |
| HubSpot         | `hubspot`         | OAuth   | `view` ✓, `write` ✗                                                      | `hubspot_search_contacts`, `hubspot_get_contact`, `hubspot_upsert_contact`, `hubspot_add_note`                                   |
| Salesforce      | `salesforce`      | OAuth   | `view` ✓, `write` ✗                                                      | `salesforce_search_records`, `salesforce_get_record`, `salesforce_upsert_lead`, `salesforce_log_activity`                        |
| Cal.com         | `calcom`          | API key | `view` ✓, `book` ✓, `manage` ✗                                           | `calcom_list_event_types`, `calcom_check_availability`, `calcom_create_booking`, `calcom_cancel_booking`                         |

Each tool maps to one operation group — disabling the group removes
every tool it gates from all attached agents.

## Endpoints

| Method   | Path                                                         | Description                                   |
| -------- | ------------------------------------------------------------ | --------------------------------------------- |
| `GET`    | `/v1/provider-connections`                                   | List connections + provider catalog           |
| `GET`    | `/v1/provider-connections/{connection_id}`                   | Retrieve one connection                       |
| `PATCH`  | `/v1/provider-connections/{connection_id}`                   | Update connection-level `allowed_operations`  |
| `DELETE` | `/v1/provider-connections/{connection_id}`                   | Disconnect                                    |
| `POST`   | `/v1/provider-connections/{provider}/api-key`                | Connect an API-key provider (Cal.com)         |
| `PUT`    | `/v1/provider-connections/{connection_id}/agents/{agent_id}` | Attach an agent (optional per-agent override) |
| `DELETE` | `/v1/provider-connections/{connection_id}/agents/{agent_id}` | Detach an agent                               |
| `POST`   | `/v1/provider-connections/{connection_id}/sheets/inspect`    | Inspect a Google Sheet by URL                 |
| `POST`   | `/v1/provider-connections/{connection_id}/sheets/configure`  | Pick the target spreadsheet + sheet           |

`{connection_id}` is the connection's **UUID**. All writes require the
`admin` role. (The browser-only OAuth start/callback routes are not
part of the public API.)

## Connection object

```json theme={null}
{
  "id": "9e1f6a3c-…",
  "provider": "slack",
  "provider_account_email": "ops@acme.com",
  "provider_account_id": "T024BE7LD",
  "provider_metadata": { "team_name": "Acme" },
  "granted_scopes": ["channels:read", "channels:history", "groups:read", "groups:history"],
  "allowed_operations": { "view": true, "post": true, "search": true },
  "effective_operations": { "view": true, "post": false, "search": true },
  "token_expires_at": null,
  "created_at": "2026-04-20T18:24:10.113Z",
  "updated_at": "2026-04-20T18:24:10.113Z",
  "agent_attachments": [
    {
      "agent_id": 12,
      "agent_name": "Customer Support Agent",
      "allowed_operations": null,
      "tool_names": ["slack_list_channels", "slack_post_message", "slack_search_messages"]
    }
  ]
}
```

| Field                                            | Type              | Description                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                             | UUID              | Connection id                                                                                                                                                                                                                                                                                                                                                                                                       |
| `provider`                                       | string            | Provider key (see table above)                                                                                                                                                                                                                                                                                                                                                                                      |
| `provider_account_email` / `provider_account_id` | string            | Identity of the connected account                                                                                                                                                                                                                                                                                                                                                                                   |
| `provider_metadata`                              | object            | Provider-specific info (e.g. the configured spreadsheet for Sheets)                                                                                                                                                                                                                                                                                                                                                 |
| `granted_scopes`                                 | array of string   | OAuth scopes actually granted                                                                                                                                                                                                                                                                                                                                                                                       |
| `allowed_operations`                             | object            | Connection-level operation toggles as configured, e.g. `{"view": true, "post": false}`. This is the raw ceiling — it does **not** reflect whether the OAuth grant covers each operation's required scopes                                                                                                                                                                                                           |
| `effective_operations`                           | object            | Read-only, additive field. The same toggles after intersecting with `granted_scopes` — i.e. what's actually enforced. An operation can show `true` in `allowed_operations` but `false` here if the grant is missing a required scope (re-consent narrowed, a scope was added to an operation after the connection was created, etc.). Use this field, not `allowed_operations`, to know what an agent can really do |
| `token_expires_at`                               | timestamp \| null | Access-token expiry (refreshed automatically)                                                                                                                                                                                                                                                                                                                                                                       |
| `agent_attachments`                              | array             | Attached agents. `allowed_operations: null` means "inherit the connection's"; `tool_names` is the resulting tool list — connection `allowed_operations`, the per-agent override, and `granted_scopes` all intersected                                                                                                                                                                                               |

Tokens and API keys are **never** returned by any endpoint.

***

## List connections

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/provider-connections \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response theme={null}
{
  "configured": true,
  "providers": {
    "slack": {
      "label": "Slack",
      "configured": true,
      "auth_kind": "oauth",
      "default_operations": { "view": true, "post": false, "search": true }
    }
  },
  "connections": [ /* Connection objects */ ],
  "agents": [{ "id": 12, "name": "Customer Support Agent" }]
}
```

`providers` is the full catalog with per-provider availability
(`configured` is false when the ThunderPhone deployment lacks that
provider's OAuth app). `agents` is a convenience list for building
attachment UIs.

***

## Update allowed operations

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH https://api.thunderphone.com/v1/provider-connections/9e1f6a3c-… \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"allowed_operations": {"view": true, "post": true, "search": true}}'
  ```
</CodeGroup>

`allowed_operations` must be an object whose keys are that provider's
operation groups (unknown keys are rejected with `400`). Returns the
updated [connection object](#connection-object).

<Warning>
  Enabling an operation the OAuth grant doesn't cover (e.g. turning on
  Slack `post` when `chat:write` wasn't granted) makes the tool fail at
  call time — reconnect from the dashboard to grant the extra scopes.
</Warning>

## Authorization model

There are three layers, evaluated together at tool-synthesis and
tool-execution time:

1. **Connection-level `allowed_operations`** — the ceiling for the
   whole connection, set via [PATCH](#update-allowed-operations).
2. **Per-agent override** (`agent_attachments[].allowed_operations`)
   — an optional narrower subset for one agent, set via
   [attach/detach](#attach-detach-agents). `null` means "inherit the
   connection's ceiling" — an override can only turn operations
   *off*, never grant one the connection ceiling denies.
3. **`granted_scopes`** — what the OAuth grant actually covers. Each
   operation requires specific scopes (e.g. Slack `post` requires
   `chat:write`); an operation enabled at both the connection and
   agent level still can't run if the grant doesn't cover it.

`effective_operations` on the [connection object](#connection-object)
is operations (1) and (3) intersected — it does **not** factor in a
per-agent override, since that's per-attachment. The `tool_names` on
each `agent_attachments` entry is the fully-intersected result: connection
ceiling ∩ per-agent override (if any) ∩ granted scopes. That list is
what the agent's tool schema actually contains, and `execute_provider_tool`
re-checks the same intersection at call time (defense in depth against a
stale route or shared connection dispatching a call the agent's current
config would no longer allow).

<Warning>
  **Authorization is provider-level, not per-record.** Toggling an
  operation on grants (or denies) it across every record the OAuth
  scope reaches — there is no per-channel, per-contact, per-object, or
  per-calendar scoping. Concretely:

  * An agent with Slack `post: true` can post to **any** channel the
    connected bot/user can reach — not a chosen subset of channels.
  * An agent with Slack `view` / `search` can read **any** channel the
    grant reaches, same caveat.
  * An agent with HubSpot or Salesforce `write: true` can create or
    update **any** record type/object the OAuth grant covers, not a
    restricted list of pipelines or object types.
  * Google Calendar operations always act on the connected account's
    **primary calendar** — there's no way to scope an agent to a
    secondary or shared calendar.

  If you need to isolate what one agent can touch within a provider
  (e.g. "this agent may only post in `#support`"), connect a
  **separate provider account** scoped to just that resource (a
  dedicated Slack app/bot invited to one channel, a HubSpot user with
  restricted CRM permissions, a secondary Google account whose primary
  calendar is the one you want exposed) and attach that connection
  instead. Per-resource ACLs on a single connection (e.g. an
  allow-list of Slack channel IDs) are **not implemented** — this has
  come up before as a half-built field with no way to populate it, and
  is tracked as a roadmap item rather than something to configure
  today.
</Warning>

## Disconnect

`DELETE /v1/provider-connections/{id}` returns `204 No Content` and
removes the connection and every agent attachment.

***

## Connect with an API key

Only for `auth_kind: "api_key"` providers — currently **Cal.com**.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/provider-connections/calcom/api-key \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"api_key": "cal_live_…"}'
  ```
</CodeGroup>

The key is validated against the provider before saving; invalid keys
return `400` with an `api_key` field error. Returns `201 Created` with
the [connection object](#connection-object).

***

## Attach / detach agents

Attaching gives the agent the connection's tools on its next deploy
of call config (tool lists are computed per call).

<CodeGroup>
  ```bash Attach (inherit connection ops) theme={null}
  curl -X PUT https://api.thunderphone.com/v1/provider-connections/9e1f6a3c-…/agents/12 \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{}'
  ```

  ```bash Attach (narrower override) theme={null}
  curl -X PUT https://api.thunderphone.com/v1/provider-connections/9e1f6a3c-…/agents/12 \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"allowed_operations": {"view": true, "post": false, "search": false}}'
  ```
</CodeGroup>

| Field                | Type           | Required | Description                                                               |
| -------------------- | -------------- | -------- | ------------------------------------------------------------------------- |
| `allowed_operations` | object \| null | no       | Per-agent override. Omit / `null` to inherit the connection-level toggles |

`PUT` is an upsert — repeat it to change the override. Returns the
[connection object](#connection-object) with updated
`agent_attachments`. `DELETE` on the same path detaches the agent
(`204`).

***

## Configure the target Google Sheet

`sheets_append_row` writes to one configured spreadsheet + sheet per
connection. Two-step setup (Sheets connections only — other providers
return `404` on these routes):

### Inspect a spreadsheet

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/provider-connections/9e1f6a3c-…/sheets/inspect \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"spreadsheet_url": "https://docs.google.com/spreadsheets/d/1AbC…/edit"}'
  ```
</CodeGroup>

```json Response theme={null}
{
  "spreadsheet_id": "1AbC…",
  "title": "Lead intake",
  "sheets": ["Leads", "Archive"]
}
```

Accepts a full URL or a bare spreadsheet id. `400` when the sheet is
inaccessible to the connected account.

### Configure

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/provider-connections/9e1f6a3c-…/sheets/configure \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"spreadsheet_id": "1AbC…", "sheet_name": "Leads"}'
  ```
</CodeGroup>

Reads row 1 of the chosen sheet as the column headers (at least one
non-empty header required — `400` otherwise) and stores
`spreadsheet_id`, `sheet_name`, and the header map in
`provider_metadata`. Returns the updated
[connection object](#connection-object). The agent tool then appends
one row per call, mapping its arguments onto those headers.

***

## Related

<CardGroup cols={2}>
  <Card title="Agents" icon="robot" href="/api-reference/agents">
    Attached tools appear alongside the agent's other tools.
  </Card>

  <Card title="MCP Servers" icon="server" href="/api-reference/mcp-servers">
    Bring arbitrary tools via the Model Context Protocol.
  </Card>

  <Card title="Integrations" icon="plug" href="/api-reference/integrations">
    Wrap any HTTP API as an agent tool.
  </Card>

  <Card title="Calls" icon="phone" href="/api-reference/calls">
    Tool invocations show up in call history.
  </Card>
</CardGroup>
