# 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.