Connectors API
A connector is a managed integration — Calendly, HubSpot, Pipedrive, Jira, Slack, Google, Twilio and the rest — that an agent can call mid-call without you writing an HTTP tool for it. You connect a provider once per workspace (sign in with OAuth, or paste a key), then add it to an agent; the agent gets one tool per provider, carrying every action the connection may perform, and InfraVoice handles the credential, the refresh and the vendor's API for you.
Base URL:
https://ultravoice-dev.us.inc/api/v1/connectorsAuth:Authorization: Bearer usf-…— the same organization key as everywhere else (a dashboard JWT works identically). Connecting, disconnecting and adding to an agent need an owner, admin or manager role; the key acts as the organization's owner. Workspace: a connection belongs to one workspace. The{workspace_id}in the path must be one of your organization's workspaces — anything else is403. Response shape: this is a Python-backed service. Listings return{"data": [...]}; errors return{"detail": "…"}— there is nosuccess/errorenvelope here. The agent-side calls at the end of this page (/api/v1/agents/…) use the Go envelope described on the Agents page.
Endpoints
Catalog and connections
| Method | Endpoint | Purpose | Role |
|---|---|---|---|
GET | /connectors | The provider catalog: doors, credential fields, actions | any member |
GET | /workspace/{workspace_id}/connections | Connections in a workspace, with health and usable actions | any member |
POST | /workspace/{workspace_id}/connect/{provider} | Start OAuth — returns the consent URL | owner/admin/manager |
POST | /workspace/{workspace_id}/connect-key/{provider} | Connect with a pasted key or token (verified before saving) | owner/admin/manager |
GET | /workspace/{workspace_id}/connections/{connection_id}/agents | Which agents use a connection | any member |
DELETE | /workspace/{workspace_id}/connections/{connection_id} | Disconnect (revokes the token, disables bound tools) | owner/admin/manager |
GET | /workspace/{workspace_id}/connections/{connection_id}/resources/{kind} | Browse what an action can be pinned to — spreadsheets (?q=) or sheets (?spreadsheet_id=), Google only today | any member |
Adding a connector to an agent — these live on the Agents API:
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/agents/{id}/tools with {"connector": "calendly"} | One tool bound to the workspace's connection, all usable actions |
PUT | /api/v1/agents/{id} with {"connectors": ["calendly", "pipedrive"]} | Same, for several at once, while updating the agent |
PUT | /api/v1/agents/{id}/tools/{toolID} | Narrow the actions, edit what the agent is told, pin fixed values |
DELETE | /api/v1/agents/{id}/tools/{toolID} | Remove the connector from the agent |
Quickstart
From nothing to an agent that books on Calendly and writes the caller into Pipedrive.
1. See what can be connected. Every provider says which door it offers — oauth_available for sign-in, credential_fields for a key — and lists its actions.
curl https://ultravoice-dev.us.inc/api/v1/connectors/connectors \
-H "Authorization: Bearer usf-YOUR_ORG_KEY"2a. Connect by signing in (OAuth). The response is a consent URL. Send the person who owns the account there — a browser redirect, a link in an email, a popup — and when they approve, the provider redirects back to InfraVoice, which stores the tokens and verifies them with one real call. Nothing else is required from you.
curl -X POST https://ultravoice-dev.us.inc/api/v1/connectors/workspace/$WS/connect/calendly \
-H "Authorization: Bearer usf-YOUR_ORG_KEY"
# → { "auth_url": "https://auth.calendly.com/oauth/authorize?client_id=…&state=…" }2b. …or connect with a key. The body's keys are the provider's credential_fields from step 1. The key is checked against the provider before it is saved — a bad token is refused, not stored.
curl -X POST https://ultravoice-dev.us.inc/api/v1/connectors/workspace/$WS/connect-key/pipedrive \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"api_key": "…"}'
# → { "data": { "connection_id": "db581881-…", "provider_id": "pipedrive" } }3. Add them to the agent. Name the connector; the server finds the workspace's live connection and binds every action it may call.
curl -X PUT https://ultravoice-dev.us.inc/api/v1/agents/$AGENT \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"connectors": ["calendly", "pipedrive"]}'4. Publish. Like every write on the agent, this edits the draft; POST /api/v1/agents/{id}/versions/publish makes it live.
The catalog
GET /connectors returns every active provider. What matters per row:
| Field | Meaning |
|---|---|
id | The provider id you use in every other call (calendly, hubspot, google, …). |
name | Display name. Also accepted wherever id is, case-insensitively. |
oauth_available | true when this deployment can start an OAuth flow for the provider — POST /connect/{provider} works. |
credential_fields | The form for the key door: [{key, label, secret, help, pattern?, pattern_help?}]. Empty for OAuth-only providers such as Google. Both doors exist on HubSpot, Slack, Calendly and Jira. |
products | What the provider covers: crm, scheduling, helpdesk, messaging, payments, issues, itsm, or for Google calendar / gmail / sheets. |
actions | [{name, description, parameters, product}] — every action the provider offers. name is the capability id (calendly__create_booking_link); parameters is the JSON Schema the model fills in on a call. |
{
"data": [
{
"id": "calendly",
"name": "Calendly",
"description": "Let your AI agent offer booking links, look up upcoming Calendly meetings, and cancel them.",
"auth_kind": "api_key",
"oauth_available": true,
"products": ["scheduling"],
"credential_fields": [
{ "key": "api_key", "label": "Personal access token", "secret": true,
"help": "Calendly → Integrations & apps → API & webhooks → Personal access tokens → Generate new token." }
],
"actions": [
{ "name": "calendly__list_event_types", "product": "scheduling",
"description": "List the meeting types that can be booked on the connected Calendly account …",
"parameters": { "type": "object", "properties": {} } },
{ "name": "calendly__create_booking_link", "product": "scheduling",
"description": "Create a single-use Calendly booking link for one meeting type …",
"parameters": { "type": "object", "required": ["event_type_uri"],
"properties": { "event_type_uri": { "type": "string", "description": "The exact uri value of the meeting type …" } } } }
]
}
]
}Providers today: Calendly, Google (Calendar, Gmail, Sheets), HubSpot, Pipedrive, Jira, Slack, Twilio, Stripe, Zendesk, Freshdesk, ServiceNow. The list is data, not code — read it rather than hard-coding it.
Connecting
Sign in (OAuth)
POST /workspace/{workspace_id}/connect/{provider} mints a single-use, signed state and returns the provider's consent URL. It is 404 for a provider that has no OAuth door here (Pipedrive, Twilio, Stripe, Zendesk, Freshdesk, ServiceNow are key-only) and 500 if the deployment has no OAuth app registered for it (oauth_available: false in the catalog told you first).
Google has several products. To ask for a subset, send {"products": ["calendar", "gmail"]} in the body; omitted means all of them. A workspace that already holds a Google connection and asks for a new product is sent through incremental consent — the person sees only the new permission, and the existing grant is kept.
The person approves on the provider's page; the provider redirects to InfraVoice's callback; InfraVoice exchanges the code, runs the provider's verify call (Calendly GET /users/me, Jira's accessible resources, …) and stores the connection labelled with the account it found. If verification fails, nothing is stored and the token is revoked at the vendor. The browser then lands on Workspaces → Integrations with ?connected=calendly or ?error=…&reason=….
Your integration does not see the callback. To know when the connection landed, poll GET /workspace/{workspace_id}/connections until a row for the provider appears with health_status: "ok".
Paste a key
POST /workspace/{workspace_id}/connect-key/{provider} takes a JSON object keyed by the provider's credential_fields:
| Provider | Body |
|---|---|
calendly, hubspot, pipedrive, slack, stripe | {"api_key": "…"} |
jira | {"site": "acme.atlassian.net", "email": "…", "api_token": "…"} |
zendesk | {"subdomain": "acme", "email": "…", "api_token": "…"} |
freshdesk | {"domain": "acme.freshdesk.com", "api_key": "…"} |
servicenow | {"instance": "acme.service-now.com", "username": "…", "password": "…"} |
twilio | {"account_sid": "AC…", "auth_token": "…", "from_number": "+1…"} |
The credential is verified with one real request to the provider before anything is written; the response tells you which of the three things went wrong:
400 … rejected these credentials — check the token— the provider said the credential is not valid.400 … accepted the credential but refused this action — the app is missing a permission (scope)— genuine credential, insufficient rights (a HubSpot private app without the CRM scopes, a Calendly app without the scopes it needs).400 … returned not found — check the subdomain— the per-tenant host (site,subdomain,instance) is wrong.
On success: {"data": {"connection_id": "…", "provider_id": "…"}}. Reconnecting a provider that is already connected replaces the credential in place — the connection id stays the same, so tools bound to it keep working. The secret is encrypted at rest and never returned by any endpoint.
Reading connections
GET /workspace/{workspace_id}/connections:
{
"data": [
{
"id": "f8e92b76-b576-4b04-ac58-93a362a57144",
"workspace_id": "c51b10f4-…",
"provider_id": "calendly",
"connector_name": "Calendly",
"account_label": "ops@acme.com",
"auth_mode": "oauth",
"is_active": true,
"health_status": "ok",
"health_detail": null,
"health_checked_at": "2026-09-22T17:40:42Z",
"token_expires_at": "2026-09-22T19:40:42Z",
"granted_scopes": ["event_types:read", "scheduled_events:read", "scheduled_events:write", "scheduling_links:write", "users:read"],
"enabled_products": ["scheduling"],
"all_products": ["scheduling"],
"actions": [
{ "name": "calendly__create_booking_link", "product": "scheduling", "enabled": true, "description": "…", "parameters": { … } }
]
}
]
}health_statusisok,reconnect_needed(the provider refused the credential on a real call — reconnect) orerror.health_detailsays why. It is written from real calls: every action the agent runs updates it, so a connection that goes bad shows here before anyone reports it.actions[].enabledisfalsefor an action the OAuth grant does not cover (a Google connection made for Calendar only cannotgoogle_gmail__send_email). Adding the product re-consents and enables it.token_expires_atis informational — access tokens are refreshed automatically.
Disconnecting
DELETE /workspace/{workspace_id}/connections/{connection_id} revokes the token at the vendor where the vendor supports it, purges the stored secret, and disables — does not delete — every agent tool bound to the connection, marking them so that reconnecting the same provider re-enables them with their settings intact. GET …/connections/{connection_id}/agents beforehand lists what will be affected:
{ "data": [ { "agent_id": "97bf8209-…", "agent_name": "Dental Clinic Intake Agent",
"tool_id": "aa6c7652-…", "tool_name": "calendly__create_booking_link", "action_count": 1 } ] }Adding a connector to an agent
A connector reaches an agent as a tool whose connector_binding names the connection and the actions the agent may call. You can write that binding by hand (POST /agents/{id}/tools with connector_binding: {connection_id, actions: [...]} — see the Agents page), but you do not have to: name the provider and the server does the rest.
One connector
curl -X POST https://ultravoice-dev.us.inc/api/v1/agents/$AGENT/tools \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"connector": "calendly"}'{
"success": true,
"data": {
"id": "tool_9c31",
"agent_id": "agt_0d5f7c1a",
"name": "calendly",
"description": "Calendly (ops@acme.com)",
"function_schema": { "parameters": { "type": "object", "properties": {}, "required": [] } },
"connector_binding": {
"connection_id": "f8e92b76-b576-4b04-ac58-93a362a57144",
"actions": [
{ "name": "calendly__list_event_types", "description": "…", "parameters": { … } },
{ "name": "calendly__create_booking_link", "description": "…", "parameters": { … } },
{ "name": "calendly__list_upcoming_events", "description": "…", "parameters": { … } },
{ "name": "calendly__get_event_invitees", "description": "…", "parameters": { … } },
{ "name": "calendly__cancel_event", "description": "…", "parameters": { … } }
]
},
"is_active": true
},
"error": null
}connector resolves the provider (by id or name) to the workspace's live, healthy connection and binds every action the grant allows. name defaults to the provider id and description to the provider and account; send your own to override either. function_schema is left empty on purpose — each action carries its own parameters, and the model calls actions, not the tool. Sending connector together with an explicit connector_binding.connection_id is refused (400) — pick one source of truth.
Several, while updating the agent
PUT /api/v1/agents/{id} accepts connectors, a list of provider ids or names. Each becomes a connector tool as above. It adds: it never removes a tool, and it is idempotent — a tool of that name that already exists (including one left disabled by an earlier disconnect) is re-bound to the live connection and re-activated rather than duplicated.
curl -X PUT https://ultravoice-dev.us.inc/api/v1/agents/$AGENT \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"system_prompt": "You are Ava, the booking assistant for Acme Dental …",
"connectors": ["calendly", "pipedrive"]}'Narrowing what the agent may do
The bound tool is an ordinary tool row. PUT /api/v1/agents/{id}/tools/{toolID} with a connector_binding that lists fewer actions restricts the agent to those; each action may also carry description (what this agent is told about when to use it) and fixed (parameter values pinned per agent, with {{variable}} interpolation from call context):
curl -X PUT https://ultravoice-dev.us.inc/api/v1/agents/$AGENT/tools/$TOOL \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{
"connector_binding": {
"connection_id": "f8e92b76-b576-4b04-ac58-93a362a57144",
"actions": [
{ "name": "calendly__create_booking_link",
"description": "Create the booking link once the caller has confirmed their name and email.",
"fixed": { "event_type_uri": "https://api.calendly.com/event_types/7f0c…" } }
]
}
}'A fixed parameter is removed from what the model sees and supplied on every call — the way to stop a model guessing a spreadsheet id, a Slack channel or a meeting type.
Google Sheets: pick the spreadsheet, don't paste it. google_sheets__append_row needs a spreadsheet_id and a tab (sheet); google_sheets__read_range a spreadsheet_id and a range. These are the operator's to choose once per agent, not the caller's to say. In the dashboard (agent → Tools → the Google tool → an action's Settings → Fixed values) the spreadsheet is a searchable list of the account's sheets and the tab a dropdown; through the API the same list is GET /connectors/workspace/{workspace_id}/connections/{connection_id}/resources/spreadsheets?q=leads → {"data":[{"id":"1abc…","name":"Leads 2026","modified_at":"…","url":"…"}]} and …/resources/sheets?spreadsheet_id=1abc… → {"data":[{"id":0,"title":"Leads","index":0}]}. Pin them with fixed as above; fixed_labels ({"spreadsheet_id": "Leads 2026"}) is optional display text the dashboard keeps beside the id. A Google connection made before the listing scope existed answers 403 with needs_scope and reconnect_products: ["sheets"] — start OAuth for those products (incremental consent) and the list works; pasting the sheet's link works regardless. An agent whose Sheets action has no spreadsheet pinned is reported in its readiness line after a build.
Remove the connector from the agent with DELETE /api/v1/agents/{id}/tools/{toolID}; the workspace connection is untouched.
From the chat
Ava does all of this for you during a build: when a request implies a scheduling tool, CRM or help desk, the interview asks which provider to use (or none), offers to connect an unconnected one right there — sign in or paste a key — or lets you skip, and the plan binds the tools. The endpoints above are the same operations, for when you drive InfraVoice from code.
Errors
Connector-service errors are {"detail": "…"} with these statuses:
| HTTP | When |
|---|---|
| 400 | The key form is missing a field (API token is required), or verification refused it — the detail says whether the credential, its permissions, or the host was wrong. |
| 401 | Missing or malformed Authorization, an expired session, or an unknown/revoked usf- key. |
| 403 | The workspace is not yours, or the action needs an owner/admin/manager role. |
| 404 | Unknown provider, a provider with no OAuth door on POST /connect/{provider}, or a connection id that is not in your workspace. |
| 500 | OAuth client id not configured for this provider — the deployment has no OAuth app for it; use the key door or ask your administrator. |
Agent-side errors (the Go envelope, success: false):
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST | connector sent together with connector_binding.connection_id. |
| 404 | UNKNOWN_CONNECTOR | connector / connectors named a provider the catalog does not have. |
| 409 | CONNECTOR_NOT_CONNECTED | The provider exists but this workspace has no live, healthy connection for it (or the connection has no usable action). Connect it first. |
| 404 | NOT_FOUND | A hand-written connector_binding.connection_id that is not a live connection in the agent's workspace — a foreign id is indistinguishable from a missing one. |
| 409 | TOOL_NAME_TAKEN | POST /tools with a name the agent already has. Use PUT /agents/{id} connectors (idempotent) or update the existing tool. |