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

# Integrations and Custom Webhooks

> Connect your CRM, helpdesk, calendar or own server, then give the tools to an agent for use during calls

## Overview

An **integration** is a connection from your workspace to a service. You enter the service's credentials once. 60db checks them with a live test, then stores them encrypted. You then attach the connection's tools to an agent. During a call, the agent can use those tools. 60db runs each tool on its servers with your stored credentials. The caller does not see any of this.

There are three kinds of connection:

| Kind | Example | Where the tool runs |
| - | - | - |
| Key provider | HubSpot, Zendesk, Slack | 60db calls the provider with its own adapter |
| Custom MCP server | Your own MCP endpoint | 60db calls your server with the MCP protocol |
| Custom webhook | Your own HTTPS endpoint | 60db sends a signed request to your endpoint |

The API reference starts at [List integration catalog](https://docs.60db.ai/api-reference/agents/list-integration-catalog.md).

## The catalog

The catalog lists 24 entries. Two are custom kinds: the Custom MCP Server and the Custom Webhook. The other 22 are providers.

Each entry connects in one of three ways:

* **Key or token.** You enter a key, token or account details. You can connect these now.
* **Custom.** You enter a URL. This applies to the Custom MCP Server and the Custom Webhook.
* **Not connectable here.** These entries cannot be connected with a key yet. Salesforce, Google Calendar and Google Drive need an app setup. Avaya uses a telephony guide. WhatsApp has its own feature.

| Category | Provider | Connects with | Fields | Curated actions |
| - | - | - | - | - |
| CRM | HubSpot | Key | Private app token | `search_contact`, `create_contact`, `create_note`, `log_call` |
| CRM | Salesforce | Not yet. Needs an app setup | | |
| Calendar | Google Calendar | Not yet. Needs an app setup | | |
| Scheduling | Calendly | Key | Personal access token | `list_event_types`, `get_available_times`, `create_scheduling_link` |
| Scheduling | Cal.com | Key | API key | `list_event_types`, `get_available_slots`, `create_booking` |
| Customer Support & CX | Freshdesk (Alpha) | Key | Domain, API key | `create_ticket`, `get_ticket` |
| Customer Support & CX | Intercom | Key | Access token | `find_contact`, `create_ticket`, `add_conversation_note` |
| Customer Support & CX | Zendesk | Key | Subdomain, agent email, API token | `create_ticket`, `get_ticket`, `search_tickets_by_email` |
| Customer Support & CX | Plain (Private Alpha) | Key | API key | `upsert_customer`, `create_thread` |
| Developer Tools | Cursor | Key | API key | `launch_agent`, `get_agent` |
| Developer Tools | Jira | Key | Site, account email, API token | `create_issue`, `get_issue` |
| Developer Tools | Custom MCP Server | Custom | URL, optional bearer token, optional headers | Tools from your server |
| Developer Tools | Custom Webhook | Custom | Actions (1 to 20, each with name, URL, method, description, parameters and body template), optional headers | One tool per action |
| IT Service Management | ServiceNow | Key | Instance, user name, password | `create_incident`, `get_incident` |
| IT Service Management | PagerDuty (Alpha) | Key | Routing key | `trigger_incident` |
| Knowledge Base | Google Drive | Not yet. Needs an app setup | | |
| Messaging | Slack | Key | Bot token | `post_message` |
| Messaging | Telegram | Key | Bot token | `send_message` |
| Messaging | Twilio | Key | Account SID, auth token, from number | `send_sms` |
| Messaging | WhatsApp | Its own feature. See [WhatsApp](https://docs.60db.ai/features/whatsapp.md) | | |
| Search | Exa | Key | API key | `web_search` |
| Search | Parallel | Key | API key | `web_search` |
| Search | Tavily | Key | API key | `web_search` |
| Telephony | Avaya (Alpha) | A telephony guide. Not connected here | | |

The [catalog endpoint](https://docs.60db.ai/api-reference/agents/list-integration-catalog.md) returns each field with its `key`, and the field `key` is what you send in `fields`.

## Connect an integration

Send the provider key and the field values. Use the keys from the catalog.

```bash theme={null}
curl -X POST https://api.60db.ai/agents/integrations \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "hubspot",
    "name": "HubSpot (sales)",
    "fields": { "token": "pat-YOUR_TOKEN" }
  }'
```

Some rules apply to every connection:

* `name` is optional. It defaults to the catalog name. Names are unique in the workspace, up to 64 characters.
* A text value is up to 4,096 characters. Secret values are never returned. The response shows a hint of the last four characters.
* A field that is not in the catalog for that provider is refused.
* If the input is invalid, nothing is stored. The request fails with `400` or `422`.
* If the input is valid but the live test fails, the connection is stored with `status: "error"` and `last_error`. The request still returns `201`. Fix the fields and test again.

To change a connection, use [Update integration](https://docs.60db.ai/api-reference/agents/update-integration.md). Send only the fields that change. A secret you leave out keeps its stored value. If you change a destination field, the stored secrets are dropped and you must send any required secret again. A custom webhook has no required secret, so its headers are dropped without an error. See [Changing an endpoint](#changing-an-endpoint).

## The live test

Every connect and every change runs a live test. [Test integration](https://docs.60db.ai/api-reference/agents/test-integration.md) runs it again on demand. The test has an 8 second limit.

* **Key providers.** The provider's own check runs with your credentials.
* **Custom MCP server.** 60db connects to the server and checks that it answers.
* **Custom webhook.** 60db sends a signed `ping` to the URL of the first action. See [The ping test](#the-ping-test).

`status` is `connected` after a passing test and `error` after a failing one. `last_error` holds a fixed message. The provider's own error text is never shown.

## Attach tools to an agent

Pick the tools an agent may use with `integration_tools` on [Create agent](https://docs.60db.ai/api-reference/agents/create-agent.md) or [Update agent](https://docs.60db.ai/api-reference/agents/update-agent.md). Each pick is a connection and a tool key:

```json theme={null}
"integration_tools": [
  { "connection_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "tool": "search_contact" },
  { "connection_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "tool": "create_note" }
]
```

List the tool keys for a connection with [List integration tools](https://docs.60db.ai/api-reference/agents/list-integration-tools.md).

The agent sees each tool under a runtime name. The agent response shows this name as `name`:

| Connection | Runtime name |
| - | - |
| Key provider | `<provider>__<action>`, for example `hubspot__search_contact` |
| Custom MCP server | `mcp_<server>__<tool>`, where `<server>` is the connection name in lowercase letters and digits, up to 12 characters |
| Custom webhook, action `call` (a webhook saved in the single-action form) | `webhook__<name>`, where `<name>` is the connection name in lowercase letters and digits, up to 40 characters |
| Custom webhook, any other action | `webhook_<name>__<action>`, for example `webhook_booktable__book_table`. The connection name is shortened first, so the whole name stays within 64 characters |

Limits:

* An agent has up to 16 tools in total. This counts its own `tools` and its `integration_tools` together. Over the limit returns `422 TOO_MANY_TOOLS`.
* A tool name is up to 64 characters, with letters, digits and `_` only. Two picks cannot give the same name.
* An agent that uses only integration tools does not need a `tool_webhook_url`. That URL is only for the agent's own `tools`.
* When you delete a connection, its tools are removed from every agent in the workspace. See [Delete integration](https://docs.60db.ai/api-reference/agents/delete-integration.md).
* When an update removes an action from a custom webhook, that action's tool is removed from every agent that holds it, in the same change.

## Connect integrations to agents

A connection's tools reach an agent in one of two ways:

* **Per agent.** Set `integration_tools` on [Create agent](https://docs.60db.ai/api-reference/agents/create-agent.md) or [Update agent](https://docs.60db.ai/api-reference/agents/update-agent.md). Use this when you are editing one agent.
* **Per connection.** On the **Integrations** page, open a connection and use **Connect to agents**. The dialog lists the agents and the tools of the connection, and saves the choice. With the API, use [Assign integration agents](https://docs.60db.ai/api-reference/agents/assign-integration-agents.md), which sets the agents of one connection in one request.

The per-connection request sets the answer exactly:

* Each agent you list gets exactly the tools you list for this connection. Its tools from other connections stay.
* An agent you leave out, or list with no tools, loses this connection's tools.
* Every agent that changes is updated as an agent edit is, and synced to the voice runtime.
* The change is all or nothing. Every agent is checked first, so a refusal changes nothing.

```bash theme={null}
curl -X PUT https://api.60db.ai/agents/integrations/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d/agents \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "agents": [
      { "agent_id": "9c2e1d7a-0b3f-4e6a-8f1d-5a7b9c0e2d44", "tools": ["book_table", "check_availability"] },
      { "agent_id": "4f1a7b2c-8d3e-4c5f-9a6b-1e2d3c4b5a69", "tools": [] }
    ]
  }'
```

The answer, and the `used_by` field of every connection, list the agents that hold the connection's tools:

```json theme={null}
{
  "success": true,
  "data": {
    "used_by": [
      { "agent_id": "9c2e1d7a-0b3f-4e6a-8f1d-5a7b9c0e2d44", "name": "Front desk", "tools": ["book_table", "check_availability"] }
    ]
  }
}
```

`used_by` is `[]` when no agent holds a tool of the connection. A connection that is just created is used by no agent.

When an update removes an action, the tool of that action is removed from every agent that holds it. Deleting a connection removes all its tools from every agent.

## How tools run during a call

When the agent calls one of these tools, 60db does the following:

1. Loads the connection and its stored credentials.
2. Checks the arguments against the tool's parameters.
3. Calls the provider, the MCP server, or your webhook.
4. Caps the result at 16 KB and returns it to the agent.

The tool call has a time budget of 6.5 seconds during a call. Answer quickly. The call is not forwarded to your `tool_webhook_url`.

A result larger than 16 KB is replaced with the first 2,000 characters of its JSON and a flag:

```json theme={null}
{ "truncated": true, "preview": "{\"contacts\":[{\"id\":\"..." }
```

When a tool fails, the agent gets an answer instead of an error, so the call continues:

```json theme={null}
{ "error": "tool_unavailable", "reason": "rate_limited" }
```

| `reason` | When |
| - | - |
| `auth_failed` | The provider or your endpoint rejected the credentials (401 or 403) |
| `rate_limited` | The provider or your endpoint answered 429 |
| `timeout` | No answer within the 6.5 second budget |
| `not_found` | The provider or your endpoint answered 404 |
| `bad_arguments` | The arguments do not match the tool. The answer also has a `message` that names the key. For a choice list, the message names the allowed values too |
| `tool_error` | The tool reported an error |
| `too_large` | The answer is larger than the size limit |
| `unavailable` | Any other failure. This includes other HTTP errors, a server that cannot be reached, a redirect, a non-JSON answer and a URL that fails the safety check |

Each tool call is logged with its arguments, its outcome and its latency. See [List tool events](https://docs.60db.ai/api-reference/agents/list-tool-events.md).

## Custom MCP servers

A custom MCP server is an endpoint that speaks the MCP protocol. 60db connects to it with Streamable HTTP. Legacy HTTP+SSE servers are not supported.

| Field | Rule |
| - | - |
| `url` | Required. Public `https` on port 443 |
| `bearer_token` | Optional. Sent as `Authorization: Bearer ...` |
| `headers` | Optional. One `Name: value` per line, up to 10 lines |

60db lists the server's tools when you ask for them. The list is cached for about a minute. Pick the tools you need in `integration_tools`. A tool whose parameter schema is larger than 4 KB cannot be picked, and the pick returns `422 INTEGRATION_TOOL_INVALID`.

## Custom webhooks

A custom webhook is one connection to HTTPS endpoints that you run. The connection holds shared headers and one signing secret. It also holds one or more **actions**. Each action is a tool that the agent can call during a call: it has its own URL, method, description, JSON Schema parameters and fixed values. When the agent calls a tool, 60db sends a signed request to that action's URL. Use this for your own functions, such as a booking system or a stock check.

### Create a webhook

```bash theme={null}
curl -X POST https://api.60db.ai/agents/integrations \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "webhook",
    "name": "Book table",
    "fields": {
      "headers": "X-Api-Key: YOUR_ENDPOINT_KEY",
      "actions": [
        {
          "key": "book_table",
          "name": "Book table",
          "url": "https://hooks.example.com/book",
          "method": "POST",
          "description": "Book a table for a date, time and party size. Call this only after the caller gives all three.",
          "parameters": {
            "type": "object",
            "properties": {
              "date": { "type": "string", "description": "YYYY-MM-DD" },
              "time": { "type": "string", "description": "HH:MM, 24-hour" },
              "guests": { "type": "integer", "description": "Party size" }
            },
            "required": ["date", "time", "guests"]
          },
          "body_template": { "source": "phone" }
        },
        {
          "key": "check_availability",
          "name": "Check availability",
          "url": "https://hooks.example.com/availability",
          "method": "GET",
          "description": "Check the free times on a date for a party size. Call this before booking when the caller has not chosen a time.",
          "parameters": {
            "type": "object",
            "properties": {
              "date": { "type": "string", "description": "YYYY-MM-DD" },
              "guests": { "type": "integer", "description": "Party size" }
            },
            "required": ["date", "guests"]
          }
        }
      ]
    }
  }'
```

The response includes `signing_secret`. Store it now. One secret signs the requests of every action in the connection. See [Signing](#signing).

The agent sees two tools: `webhook_booktable__book_table` and `webhook_booktable__check_availability`. Each tool's description is its action's `description`, and its parameters are its action's `parameters`. The runtime names are listed in [Attach tools to an agent](#attach-tools-to-an-agent).

### Fields

Connection fields:

| Field | Required | Rule |
| - | - | - |
| `actions` | Yes | A JSON list of 1 to 20 actions, as an array or as text. Up to 256 KB |
| `headers` | No | Shared by every action. One `Name: value` per line, up to 10 lines. Names that start with `X-60db-` are reserved. Stored as a secret |

Action fields:

| Field | Required | Rule |
| - | - | - |
| `key` | No | 1 to 40 characters: `a` to `z`, `0` to `9` and `_`. Unique in the connection. It names the tool. Left out, it is made from `name`: lower-case words joined by `_`, with `_2`, `_3` added when a key is taken |
| `name` | Yes | 1 to 80 characters. Shown in the dashboard |
| `url` | Yes | Public `https` on port 443, up to 2,048 characters. No credentials, no fragment |
| `method` | No | `GET`, `POST`, `PUT`, `PATCH` or `DELETE`. Default `POST` |
| `description` | Yes | 1 to 500 characters. The agent reads it to decide when to call the tool |
| `parameters` | No | A JSON Schema object, as text or as an object. Up to 4 KB. Default: no parameters |
| `body_template` | No | A JSON object, up to 4 KB. Sent with `POST`, `PUT` and `PATCH` only |

Send `key` on every action. The key is the tool's identity. If you change a key, the agents that hold the old tool lose it. An action sent without a key gets one from its name, so renaming it can also change its key.

**Single-action form.** The flat fields `url`, `method`, `description`, `parameters` and `body_template`, without `actions`, still work. They describe one action with the key `call`, so its tool keeps the runtime name `webhook__<name>`. On a webhook with several actions the flat fields are refused. Send `actions` instead.

### Parameters

`parameters` is a JSON Schema object with `"type": "object"`. Each property must have one of these types: `string`, `number`, `integer` or `boolean`. Every name in `required` must be a listed property. Objects, arrays and nested schemas are not supported.

```json theme={null}
{
  "type": "object",
  "properties": {
    "date": { "type": "string", "description": "YYYY-MM-DD" },
    "guests": { "type": "integer", "description": "Party size" },
    "outdoor": { "type": "boolean", "description": "Seat outside if the caller asks" }
  },
  "required": ["date", "guests"]
}
```

The agent sends only the properties you declare. Any other key it invents is removed before the request leaves 60db. Empty values are left out.

### Choice lists

A `string` property can list its allowed values with `enum`:

```json theme={null}
"plan": { "type": "string", "description": "Plan the caller chose", "enum": ["basic", "pro"] }
```

* `enum` is allowed on `string` properties only. It has 1 to 50 values. Each value is a non-empty string of up to 200 characters, and no value repeats.
* A value outside the list is refused before the request is sent. During a call, the agent receives `{ "error": "tool_unavailable", "reason": "bad_arguments", "message": "plan must be one of: basic, pro" }`. A direct action returns `422 INTEGRATION_BAD_ARGUMENTS` with the same message.
* A property the agent leaves out is not checked against the list. Put it in `required` to make it mandatory.

### How a call is sent

**`POST`, `PUT` and `PATCH`.** 60db sends a JSON body. It is built in this order:

1. The `body_template` values.
2. The arguments the agent chose. They replace a template value with the same key.
3. A `_60db` object with the connection, the tool name, and, during a live call, the call id and the agent id.

For example, with the template above and a call at 19:30 for four guests, the body is:

```json theme={null}
{
  "source": "phone",
  "date": "2026-11-05",
  "time": "19:30",
  "guests": 4,
  "_60db": {
    "connection_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "tool": "webhook_booktable__book_table",
    "call_id": "6b1f0c52-4d7e-4b8a-9d3e-2a1c8e7f0a11",
    "agent_id": "9c2e1d7a-0b3f-4e6a-8f1d-5a7b9c0e2d44"
  }
}
```

`call_id` and `agent_id` are present only during a live call. A call started with [Run an action](#run-an-action-with-curl) does not have them.

**`GET` and `DELETE`.** These methods have no body. The declared arguments are sent as the query string, for example `?date=2026-11-05&guests=4`. Values that are objects are refused with `bad_arguments`. The `body_template` is not sent.

### Your answer

Return a `2xx` status. 60db reads the answer like this:

| Your response | What the agent receives |
| - | - |
| Empty body | `{}` |
| JSON object | The object, as it is |
| Any other JSON value, such as a list or a number | `{ "result": <value> }` |
| `text/plain`, up to 4 KB | `{ "text": "..." }` |
| Any other content type that is not JSON | An error with reason `unavailable` |

A redirect is an error. 60db does not follow redirects. A non-`2xx` status is an error, and the reason follows the table in [How tools run during a call](#how-tools-run-during-a-call). Respond within the 6.5 second budget during a live call.

### Signing

60db signs every request to your endpoint. Each request has three headers:

| Header | Value |
| - | - |
| `X-60db-Event` | `tool.call` for a tool call, `ping` for the connection test |
| `X-60db-Timestamp` | Unix time in seconds |
| `X-60db-Signature` | `v1=` followed by the hex HMAC-SHA256 of `{timestamp}.{raw}` |

`raw` is the body as sent. For `POST`, `PUT` and `PATCH` it is the JSON body. For `GET` and `DELETE` it is the query string exactly as sent, without the leading `?`.

* The signing secret starts with `whsec_`. It is shown **once**, in the create response. It is not shown again. It belongs to the connection, so one secret signs the requests of every action.
* To get a new secret, use [Rotate signing secret](https://docs.60db.ai/api-reference/agents/rotate-integration-secret.md). The old secret stops working at once. Update your endpoint in the same moment.
* Changing the URL or the method keeps the signing secret.

### Changing an endpoint

Changing a webhook's destination drops its shared headers. The signing secret is kept. A change counts as a destination change when either of these is true:

* An existing action's URL or method changes. A change of path or query counts as well.
* An action is added, or moved, to an origin (scheme, host and port) that the connection did not use before the change.

Changing an action's description, parameters or body template is not a destination change, so the headers stay.

The change is saved without an error. Send `headers` again in the same request if your endpoint needs them, and check your endpoint after the change.

### Verify signatures

Verify every request before you act on it. The rules match the other 60db webhooks: the secret is used as UTF-8 text, the raw bytes are used, timestamps more than 300 seconds from your server's clock are refused, and the comparison is constant time.

The examples below are complete. They use only standard libraries.

**Node.js**

```javascript theme={null}
import crypto from 'node:crypto';
import http from 'node:http';

const SECRET = process.env.SIXTYDB_WEBHOOK_SECRET; // whsec_... from create or rotate

function verifySignature({ raw, timestamp, signature, secret }) {
  if (!/^\d+$/.test(timestamp || '') || !/^v1=[a-f0-9]{64}$/.test(signature || '')) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = 'v1=' + crypto.createHmac('sha256', secret)
    .update(timestamp).update('.').update(raw).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

http.createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const body = Buffer.concat(chunks);
    const at = req.url.indexOf('?');
    // GET and DELETE: the signed text is the query string as received.
    const raw = req.method === 'GET' || req.method === 'DELETE'
      ? (at === -1 ? '' : req.url.slice(at + 1))
      : body;
    const ok = verifySignature({
      raw,
      timestamp: req.headers['x-60db-timestamp'],
      signature: req.headers['x-60db-signature'],
      secret: SECRET,
    });
    if (!ok) { res.writeHead(401).end(); return; }

    if (req.headers['x-60db-event'] === 'ping') { res.writeHead(200).end(); return; }

    const msg = JSON.parse(body.toString('utf8') || '{}');
    const result = { booked: true, reference: `BK-${msg._60db?.call_id ?? 'manual'}` };
    res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(result));
  });
}).listen(8080);
```

**Python (Flask)**

```python theme={null}
import hashlib
import hmac
import os
import time

from flask import Flask, abort, jsonify, request

app = Flask(__name__)
SECRET = os.environ["SIXTYDB_WEBHOOK_SECRET"]  # whsec_... from create or rotate


def verify_signature(raw: bytes, timestamp: str, signature: str, secret: str) -> bool:
    if not (timestamp or "").isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    expected = "v1=" + hmac.new(
        secret.encode("utf-8"), f"{timestamp}.".encode("utf-8") + raw, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")


@app.post("/book")
def book():
    raw = request.get_data()  # the exact bytes received
    if not verify_signature(raw, request.headers.get("X-60db-Timestamp", ""),
                            request.headers.get("X-60db-Signature", ""), SECRET):
        abort(401)
    if request.headers.get("X-60db-Event") == "ping":
        return "", 200
    msg = request.get_json()
    call_id = (msg.get("_60db") or {}).get("call_id") or "manual"
    return jsonify(booked=True, reference=f"BK-{call_id}")


@app.get("/availability")
def availability():
    # GET: the signed text is the raw query string, as received.
    if not verify_signature(request.query_string, request.headers.get("X-60db-Timestamp", ""),
                            request.headers.get("X-60db-Signature", ""), SECRET):
        abort(401)
    return jsonify(available=True, slots=["19:30", "20:00"])
```

Use the raw query string, not a rebuilt one. Re-encoding the query can change its bytes, and the signature would not match.

### The ping test

When you create a connection, or run [Test integration](https://docs.60db.ai/api-reference/agents/test-integration.md), 60db sends this request to the URL of the first action. The ping is always a `POST`, whatever method that action uses:

```http theme={null}
POST /book HTTP/1.1
X-60db-Event: ping
X-60db-Timestamp: 1762165200
X-60db-Signature: v1=...
Content-Type: application/json
```

```json theme={null}
{ "type": "ping", "connection_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" }
```

Any `2xx` status means the connection is connected. The body is not read. Any other status is an error, and the connection is stored with `status: "error"`.

The create request sends the first ping before it returns the signing secret. So you do not have the secret yet when that ping arrives. An endpoint that verifies signatures refuses it, and the connection is stored with `status: "error"`. Store the `signing_secret` from the response, then run [Test integration](https://docs.60db.ai/api-reference/agents/test-integration.md). The new test passes when your endpoint answers `2xx`.

## Run an action with curl

You can run one action of a connection without a call. Use this to check a connection, or to run a step from your own code. The action runs the same way as during a call. It has the same argument checks and the same 16 KB result cap, with a 15 second budget.

```bash theme={null}
curl -X POST https://api.60db.ai/agents/integrations/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d/actions/search_contact \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"arguments": {"email": "jane@acme.com"}}'
```

```json theme={null}
{
  "success": true,
  "data": {
    "ok": true,
    "result": { "contacts": [] }
  }
}
```

`action` must be one of the tools that [List integration tools](https://docs.60db.ai/api-reference/agents/list-integration-tools.md) returns for the connection. For a custom webhook, `action` is the action's `key`, for example `book_table`. Provider failures return `502` with a `code` that names the cause. Your own rate limit returns `429`. See [Run integration action](https://docs.60db.ai/api-reference/agents/run-integration-action.md) for every status and code.

## Limits and roles

| Limit | Value |
| - | - |
| Tools per agent | 16 in total, including the agent's own tools |
| Actions per custom webhook | 20 |
| Choice list (`enum`) | 50 values per property, 200 characters each |
| Tool result to the agent | 16 KB. A larger result is cut |
| Tool time budget during a call | 6.5 seconds |
| Run an action | 15 seconds |
| Test a connection | 8 seconds |
| Create, update, rotate a secret (per user, per minute) | 10 |
| Test, and list tools (per user, per minute) | 30 |
| Run an action (per user, per minute) | 60 |
| Request an integration (per user, per minute) | 5 |

| Action | Roles |
| - | - |
| See the catalog and connections | Owner, admin, developer, member, viewer |
| Request an integration | Owner, admin, developer, member, viewer |
| List tools, run an action | Owner, admin, developer, member |
| Connect, change, test, delete, rotate a secret | Owner, admin, developer |
| Give a connection's tools to agents (`PUT /agents/integrations/{id}/agents`) | Needs both permission to use integrations and permission to edit agents. Without both, `403 POLICY_DENY` |

API keys need the `agents` scope.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.