# Calling tools

> List the tools, call one over REST, read the response, and offer the tools to a model through tool calling.

Tools live under the same base URL as chat completions, `https://api.clonvo.com/v1`, and use the same `Authorization: Bearer` header.

| Endpoint | Description |
| --- | --- |
| `GET /v1/tools` | Every tool, with its description, JSON Schema and the providers your organization can use right now. |
| `POST /v1/tools/{id}` | Calls one tool. The body has `arguments` and an optional [`provider` object](https://clonvo.com/docs/tool-routing.md). |
| `GET /v1/tools/jobs/{id}` | A long-running call’s [job](https://clonvo.com/docs/jobs.md). |
| `DELETE /v1/tools/jobs/{id}` | Cancels a running [job](https://clonvo.com/docs/jobs.md). |

## List tools

```bash
curl https://api.clonvo.com/v1/tools \
  -H "Authorization: Bearer $CLONVO_API_KEY"
```

```json
{
  "data": [
    {
      "id": "web_search",
      "name": "Web search",
      "description": "Search the web and get a ranked list of pages…",
      "input_schema": {
        "type": "object",
        "properties": { "query": { "type": "string" } },
        "required": ["query"]
      },
      "providers": ["…", "clonvo"],
      "available_providers": ["clonvo"]
    }
  ]
}
```

`input_schema` and `providers` are shortened here. `providers` lists the providers that can serve the tool in Clonvo’s default order: the [own-key providers](https://clonvo.com/docs/tool-keys.md) that serve it, then `clonvo` for Clonvo credits. `available_providers` lists the providers you have an enabled key for, plus `clonvo` when your organization can pay with credits (a paid plan and a balance above zero). A tool with an empty `available_providers` list can’t be called yet.

## Call a tool

Send the tool’s arguments in `arguments`. Each tool’s page lists its arguments, for example [`web_search`](https://clonvo.com/docs/web-search.md).

### curl

```bash
curl https://api.clonvo.com/v1/tools/web_search \
  -H "Authorization: Bearer $CLONVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "arguments": { "query": "PostgreSQL 18 release notes", "max_results": 3 }
  }'
```

### Python

```python
import os
import requests

resp = requests.post(
    "https://api.clonvo.com/v1/tools/web_search",
    headers={"Authorization": f"Bearer {os.environ['CLONVO_API_KEY']}"},
    json={"arguments": {"query": "PostgreSQL 18 release notes", "max_results": 3}},
    timeout=60,
)
resp.raise_for_status()
for hit in resp.json()["result"]["results"]:
    print(hit["title"], hit["url"])
```

### Node.js

```js
const resp = await fetch("https://api.clonvo.com/v1/tools/web_search", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CLONVO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ arguments: { query: "PostgreSQL 18 release notes", max_results: 3 } }),
});
if (!resp.ok) throw new Error((await resp.json()).error.message);
const { result } = await resp.json();
for (const hit of result.results) console.log(hit.title, hit.url);
```

## Response

```json
{
  "id": "gen-01M69T8V4J2QH3K5N7P9R1S3TX",
  "tool": "web_search",
  "provider": "clonvo",
  "result": {
    "query": "PostgreSQL 18 release notes",
    "results": [
      {
        "title": "PostgreSQL 18 Release Notes",
        "url": "https://www.postgresql.org/docs/18/release-18.html",
        "snippet": "Release date: 2025-09-25. This release contains a variety of fixes…",
        "published_at": null,
        "score": 0.86
      }
    ]
  },
  "usage": { "cost": 0.001, "charged": 0.00102, "is_byok": false }
}
```

Every tool answers in this shape; only `result` differs, and each tool’s page describes its fields. The long-running tools answer with a [job](https://clonvo.com/docs/jobs.md) instead.

| Field | Description |
| --- | --- |
| `id` | The call’s generation id, also sent in the `x-clonvo-generation-id` header (errors included). It appears in **Activity** and works with [`GET /v1/generation`](https://clonvo.com/docs/other-endpoints.md#get-v1-generation). |
| `tool` | The tool you called. |
| `provider` | `clonvo` for a call on Clonvo credits, or the id of [your own provider](https://clonvo.com/docs/tool-keys.md#provider-ids) for a call on your key. |
| `result` | The tool’s result. |
| `result.notes` | Present when the provider that served the call handled an argument differently. Results can differ slightly between providers. |
| `usage` | `charged` is what Clonvo deducted from credits. On your own key, `cost` is the list price and `is_byok` is `true`. On Clonvo credits, `cost` is Clonvo’s price for the call and `is_byok` is `false`. See [Tool billing](https://clonvo.com/docs/tool-billing.md). |

Errors use the [usual error shape](https://clonvo.com/docs/errors.md); the codes specific to tools are in [Tool errors](https://clonvo.com/docs/tool-errors.md).

## With a model’s tool calling

`input_schema` is a valid function `parameters` object, so you can offer the tools to any model through [tool calling](https://clonvo.com/docs/tool-calling.md) and run the calls your model makes through `POST /v1/tools/{id}`. If your framework speaks MCP, connecting to the [MCP server](https://clonvo.com/docs/mcp.md) does this for you.

```python
import json
import os

import requests
from openai import OpenAI

BASE_URL = "https://api.clonvo.com/v1"
API_KEY = os.environ["CLONVO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

client = OpenAI(base_url=BASE_URL, api_key=API_KEY)

tools = requests.get(f"{BASE_URL}/tools", headers=HEADERS, timeout=30).json()["data"]
functions = [
    {"type": "function", "function": {"name": t["id"], "description": t["description"], "parameters": t["input_schema"]}}
    for t in tools
    if t["id"] in ("web_search", "web_scrape")
]

messages = [{"role": "user", "content": "What changed in PostgreSQL 18? Cite your sources."}]
while True:
    completion = client.chat.completions.create(model="anthropic/claude-sonnet-5-5", messages=messages, tools=functions)
    message = completion.choices[0].message
    if not message.tool_calls:
        print(message.content)
        break
    messages.append(message)
    for call in message.tool_calls:
        resp = requests.post(
            f"{BASE_URL}/tools/{call.function.name}",
            headers=HEADERS,
            json={"arguments": json.loads(call.function.arguments)},
            timeout=120,
        )
        body = resp.json()
        content = body["result"] if resp.ok else body["error"]["message"]
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(content)})
```

The example offers two tools that answer at once. The long-running tools answer with a job, so offer them only if your loop [polls the job](https://clonvo.com/docs/jobs.md) for its result. When a call fails, passing the error message back lets the model correct its arguments.
