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. Slackpost: 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.
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) |
| 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/picker-token | Mint a short-lived token for the Google Picker |
POST | /v1/provider-connections/{connection_id}/sheets/inspect | List a picked spreadsheet's tabs |
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
{
"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
curl https://api.thunderphone.com/v1/provider-connections \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"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
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}}'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.
Authorization model
There are three layers, evaluated together at tool-synthesis and tool-execution time:
- Connection-level
allowed_operations— the ceiling for the whole connection, set via PATCH. - Per-agent override (
agent_attachments[].allowed_operations) — an optional narrower subset for one agent, set via attach/detach.nullmeans "inherit the connection's ceiling" — an override can only turn operations off, never grant one the connection ceiling denies. granted_scopes— what the OAuth grant actually covers. Each operation requires specific scopes (e.g. Slackpostrequireschat: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
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).
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.
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_…"}'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.
Attach / detach agents
Attaching gives the agent the connection's tools on its next deploy of call config (tool lists are computed per call).
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 '{}'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}}'| 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 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. ThunderPhone holds the per-file drive.file Google scope,
so the connection can only open spreadsheets the user has explicitly
granted by picking them in the Google Picker (the dashboard runs this
flow). Setup is three steps (Sheets connections only — other providers
return 404 on these routes):
Mint a Picker token
curl -X POST https://api.thunderphone.com/v1/provider-connections/9e1f6a3c-…/sheets/picker-token \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{ "access_token": "ya29.…" }Returns a short-lived access token for the connection's own Google
credential. Load the Google Picker
with this token (setOAuthToken) so the user's pick grants file access
to the same credential the agent's tools use at call time.
Inspect a spreadsheet
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_id": "1AbC…"}'{
"spreadsheet_id": "1AbC…",
"title": "Lead intake",
"sheets": ["Leads", "Archive"]
}Takes the file id returned by the Picker and lists the tab names.
400 when the spreadsheet has not been granted through the Picker (or
access was revoked).
Configure
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"}'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. The agent tool then appends
one row per call, mapping its arguments onto those headers.