Connections
Connector CRUD over the API and MCP - browse the catalog, connect a source, watch its syncs, and fix it when it breaks, with display metadata on every read
Connections are how a workspace hooks up its first-party data — CRM, call recordings, comms, docs, support tickets, and tracked social handles. The whole lifecycle is addressable here: browse what can be connected, connect it, watch it sync, and fix it when it breaks. Every read is self-describing — the catalog tells you each connector's display metadata (name, logo, category) and exactly which fields its connect flow needs, so a client renders the whole surface from the API with no hardcoded connector list.
| Operation | REST | MCP | Scope | |
|---|---|---|---|---|
| The connector catalog | connections.list_catalog | GET /connections/catalog | connections tool, action catalog | connections:read |
| Connected instances | connections.list | GET /connections | connections tool, action list | connections:read |
| One connection | connections.get | GET /connections/:id | connections tool, action get | connections:read |
| Status (for polling) | connections.get_status | GET /connections/:id/status | connections tool, action status | connections:read |
| Sync-run history | connections.list_runs | GET /connections/:id/runs | connections tool, action runs | connections:read |
| Data summary | connections.get_summary | GET /connections/:id/summary | connections tool, action summary | connections:read |
| Connect a source | connections.connect | POST /connections | connections tool, action connect | connections:write |
| Disconnect | connections.disconnect | DELETE /connections/:id | connections tool, action disconnect | connections:delete |
| Set the owner | connections.update | PATCH /connections/:id | connections tool, action update | connections:write |
| Repair in place | connections.reconnect | POST /connections/:id/reconnect | connections tool, action reconnect | connections:write |
| Call filters | connections.set_call_filters | PUT /connections/:id/call-filters | — | connections:write |
| CRM recency window | connections.set_crm_recency | PUT /connections/:id/crm-recency | — | connections:write |
| Comms filter | connections.set_comms_filter | PUT /connections/:id/comms-filter | — | connections:write |
/connections/catalogauth requiredEverything that CAN be connected - one entry per connector, carrying display metadata, the connect-flow spec, and whether it is connectable right now.
/connectionsauth requiredEverything that IS connected - newest first, each row carrying live health and its connector's display block.
/connectionsauth requiredConnect a source. One endpoint covers every flow: an API key connects in one call, a handle resolves and connects, OAuth hands back an authorize URL.
Which keys can call what
The six reads authorize for every key tier, including read-only MCP keys — browsing the catalog and checking a connection's health is safe everywhere. The four lifecycle writes (connect / disconnect / update / reconnect) and the three config writes need connections:write (or connections:delete for disconnect), which is an editor-level grant:
- OAuth tokens (a human member's delegated session — Claude Desktop and other MCP clients connected via OAuth) carry the write scopes. The token re-checks the member's workspace membership on every request, so revoking the member revokes the capability.
- API key bundles do not carry the write scopes. A headless agent key can read everything here but cannot connect or disconnect a source; connector lifecycle stays with a person (or a person's delegated session).
The three set_* config writes are REST-only — they are not on the MCP tool. The connect action accepts an inline call_filters object instead, so an MCP connect of a call recorder never lands in the paused-until-configured state.
The catalog is self-describing
Each GET /connections/catalog entry carries everything a client needs to render and drive the connector with no hardcoded knowledge:
| Field | Notes |
|---|---|
type | The connector id (hubspot, gong, attio, …). What you pass to connect. |
name | Display name ("HubSpot"). |
kind | first_party. |
category | crm | calls | comms | docs | support | social — the grouping a picker renders. |
authMethod | oauth | api_key | handle | webhook — which connect flow applies. |
logo | { key, url, monogram }. url is an absolute asset URL, or null when no asset ships — then render the two-letter monogram. Never a fabricated path. |
connect | { method, fields, keyUrl } — the connect-flow spec. See below. |
connectable | Whether connect will accept this type right now; unavailableReason says why not (e.g. a coming-soon connector). |
single_instance | The effective connect rule. Read this — never the raw instancing capability — when deciding whether to offer a second connect. |
ownership | workspace (one shared connection) | personal (per-member, e.g. Gmail / Outlook — update can set its owner). |
description, dataStreams | What the connector is and what it pulls. |
The connect spec
connect.fields declares exactly what input the connect flow needs, so a form (or an agent) is generated from the catalog:
api_keyconnectors declare one field:{ key: 'api_key', label, secret: true, placeholder, help }.placeholdershows the key's shape when the vendor pins one (sk-apik_...for AskElephant) and isnullotherwise — a made-up key shape is worse than no hint.helpcarries the vendor-specific scope guidance, which is worth surfacing: most connector failures are not a wrong key but a key that cannot see enough, and that presents as an empty workspace rather than as an error.handleconnectors (X / LinkedIn) declare{ key: 'account_handle', secret: false }— the public handle to track.oauthconnectors declarefields: []— the flow is a redirect, not a form. See the OAuth handoff below.
connect.keyUrl is the vendor page where the customer creates the credential, or null when there is nowhere to send them (an OAuth redirect, a social handle, or a deployment whose token you issue yourself). Render it as a link next to the key field.
A few connectors ask for more than one field, and the spec is how you know. Never assume api_key means exactly one input — read connect.fields and render what it declares:
- Aircall declares a non-secret
api_idahead of the key, because its API is HTTP Basic and needs both halves of the pair. Sending onlyapi_keyis refused withinvalid_argument. - Computer Agent declares a non-secret
base_urlahead of the key — the deployment to drive.
Each field also carries resupplyOnReconnect, which is what a reconnect form should filter on. A credential is replaced wholesale on re-key, so every half of it must be collected again (api_key, and Aircall's api_id), while a field stored on the connection row is not (base_url — a re-key keeps the deployment it was pointed at). Guessing is silent both ways: hide a credential half and the reconnect is refused with no field to fix it; show a stored-config field and you collect a value reconnect drops.
Connecting
POST /connections takes { connector_type, ... } and routes on the connector's auth_method. The response's mode discriminator tells you what happened:
mode | When | What you got |
|---|---|---|
connected | api_key or handle connect | A live connection — the credential was stored (or the handle resolved) and the first sync was triggered. |
oauth_redirect | oauth connect | An authorize_url. Send the human there; the provider callback completes the connection server-side. Poll GET /connections/:id/status until health leaves syncing. |
already_connected | A duplicate | The existing connection, unchanged. Workspace-owned data sources are single-instance; a second connect of the same handle dedupes. |
Per-flow request fields: api_key connectors take api_key (plus api_id where the catalog declares that field, and optionally call_filters for call recorders — atomic with the connect, so the source is never created paused-but-unconfigured); handle connectors take account_handle; all flows accept an optional display name. A connector whose catalog entry says connectable: false is refused up front with connector_not_connectable.
Attio call recordings
An Attio connection syncs call recordings and transcripts alongside the CRM objects — an Attio-only workspace gets its call transcripts through the one connection, with no separate call recorder. The workspace API key needs three extra scopes for that: meeting:read, record_permission:read, and call_recording:read. A key without them still connects and keeps the CRM objects syncing; the two recording streams fail on their own while everything else flows. To turn recordings on later, re-issue the key with the extra scopes and pass it to POST /connections/:id/reconnect.
Watching
GET /connectionsrows each carry aconnectorblock —{ type, name, category, logo }— so a list renders names and logos straight off the read, plus derivedhealth(healthy|syncing|stale|degraded|needs_reauth|rate_limited|error|disconnected) andlast_run.GET /connections/:id/statusis the lean polling read:status,health,is_syncing, and nothing heavy. Use it for a connecting / syncing badge.GET /connections/:id/runsis the sync-run history, newest first — per run:status,started_at/finished_at,streams_total/streams_failed,records_written, and a bucketederror_reason(auth|rate_limit|transient|config|unknown). Raw provider error text is never surfaced.GET /connections/:id/summaryis a glanceable snapshot of the connection's own synced data — per-category stats (deals and contacts for a CRM, calls for a recorder), computed live. A connector that has not synced yet degrades to apartialsummary withnullstats, never an error;nullnever renders as0.
Fixing
POST /connections/:id/reconnect repairs a broken connection in place — it never creates a second row, so the connection's history and its warehouse data stay attached. Per flow: oauth re-mints the authorize URL, api_key takes a fresh api_key (and api_id where the catalog declares it — re-keying stores the whole pair), handle clears the error and re-triggers the sync. A connection that is already healthy is refused with invalid_argument — there is nothing to restore; disconnect first if you mean to replace it.
DELETE /connections/:id disconnects — the row is retained (status disconnected) so a later reconnect restores it in place.
On MCP
The connections coarse tool carries ten actions: the six reads (catalog / list / get / status / runs / summary) and the four lifecycle writes (connect / disconnect / update / reconnect). Reads are also reachable as native MCP resources on the connection:// scheme (connection://catalog, connection://list, connection://<id>, connection://<id>/status, connection://<id>/runs, connection://<id>/summary) for clients that implement resources/*.
Scope gating is per action: reads need connections:read (on every bundle including read-only), connect / update / reconnect need connections:write, and disconnect needs connections:delete — which in practice means an OAuth-connected client can manage connectors and an API-key client can watch them.
Sync cadence is not here
How often a connector type syncs is a platform-level operational setting, not a per-connection knob — there is no cadence field on any of these ops. If a source looks stale, read /runs first: a rate_limit or auth error_reason is the actual story far more often than cadence.