# Call a tool

`POST /v1/tools/{tool}/calls`

Needs an API key.

Call a tool and return its output.

The call is served by the cheapest provider that supports your input, falling back to the next one if it fails. Set `provider` to use one provider only, and `require` to insist on optional outputs. You are charged when the call succeeds, as shown in `cost`.

Some providers take minutes. The request waits up to `wait` seconds (25 by default, 0 to 50) for the call to finish. A call still running then answers 202 with `status: "pending"` and a `Location` header: it goes on running, and you are charged only if it succeeds. Read it at `GET /v1/calls/{call_id}?wait=50` until its status is `succeeded` or `failed`. A call is never cut short because your request ended.

A call that no provider could serve answers 502, or 504 when it ran out of time, or 429 with `PROVIDER_RATE_LIMITED` when every provider tried refused it for its rate limit: retry after the `Retry-After` header's seconds. All three carry a `call_id`: the call is recorded, was not charged, and can be read back from `GET /v1/calls/{call_id}`. One exception: a provider that fails a call and bills it ends the call there, with no fallback, and 502's `detail` says what you were charged: what that provider billed. The call's `failure_reason` is then `billed_provider_failure`. A request refused before any provider was contacted (unknown tool, invalid input, insufficient balance) records nothing.

A 503 with `CALL_RESULT_DEFERRED` means the call ran but Voxoria was too busy to return its result: it carries a `call_id` and a `Location` header. Read the call at `GET /v1/calls/{call_id}` rather than calling again, which would make, and charge, a new call.

To retry safely, send an `Idempotency-Key` header. If a call with that key already succeeded in the last 24 hours, you get that call back with the header `Idempotent-Replayed: true`: no provider is called and you are not charged again, so `cost` is what the first call cost. The key must come with the same tool and body each time; a different request answers 422 with `IDEMPOTENCY_KEY_REUSED`. While the first call is still running a retry waits for it as the first request did, and answers it, or 202 with it still running. A call that failed does not keep its key, so retrying it makes a new call. Without the header, every request is a new call.

To reuse a result you already paid for, set `reuse_within` in seconds. If your workspace has a succeeded call with the same tool, `input`, `provider` and `require` made within that window, the newest one is returned with `reused: true`: no provider is called, nothing is charged and no call is recorded, so `id` and `cost` are that earlier call's. A replay for an `Idempotency-Key` is checked first; a reused answer does not claim the key.

Set `label` to name the call, then find it with `GET /v1/calls?label=...` or change it with `PATCH /v1/calls/{call_id}`. A reused or replayed answer keeps its own label. Outputs are kept with no time limit.

Test keys cannot call tools yet and answer 403 with `TEST_KEY_NOT_ALLOWED`. Use a live key.

## Example request

cURL:

```bash
curl https://api.voxoria.ai/v1/tools/google.serp.organic/calls \
  -H "Authorization: Bearer $VOXORIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input":{"query":"crm for small teams","country":"gb"}}'
```

Python:

```python
import os

import httpx

response = httpx.post(
    "https://api.voxoria.ai/v1/tools/google.serp.organic/calls",
    headers={"Authorization": f"Bearer {os.environ['VOXORIA_API_KEY']}"},
    json={"input": {"query": "crm for small teams", "country": "gb"}},
    timeout=60,
)
print(response.json())
```

TypeScript:

```ts
const response = await fetch("https://api.voxoria.ai/v1/tools/google.serp.organic/calls", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.VOXORIA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"input": {"query": "crm for small teams", "country": "gb"}}),
});
console.log(await response.json());
```

MCP (`tools/call` params):

```json
{
  "name": "call_tool",
  "arguments": {
    "tool": "google.serp.organic",
    "input": {
      "query": "crm for small teams",
      "country": "gb"
    }
  }
}
```

## Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `tool` | string | Yes |  | The tool's ID, such as `google.serp.organic`. |

## Headers

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | string | No |  | A key of your choosing, 1 to 128 printable ASCII characters, that makes a retry safe. The same key with the same request within 24 hours returns the first call and is not charged again. |

## Request body

What to call a tool with.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `input` | object | Yes |  | The tool's input, as its input schema describes. |
| `provider` | string | No |  | Serve the call from this provider only. Omit to use the cheapest that can. 1 or more characters. Can be `null`. |
| `require` | array of string | No |  | Outputs the serving provider must return, by name. Only providers that return each optional one are used; a required one is always returned, so naming it changes nothing. Each item: 1 or more characters. Can be `null`. |
| `reuse_within` | integer | No |  | Reuse a result instead of calling again: if your workspace has a succeeded call with the same tool, `input`, `provider` and `require` made within this many seconds (1 to 2592000, 30 days), the newest such call is returned with `reused: true`. No provider is called, nothing is charged and no call is recorded; `id` and `cost` are that earlier call's. Omit to always make a new call. From 1 to 2592000. Can be `null`. |
| `label` | string | No |  | A name of your choosing for the call, 1 to 100 characters, to find it again by. Not unique. Ignored when the answer is a reused or replayed call. 1 to 100 characters. Can be `null`. |
| `wait` | integer | No | `25` | Seconds to wait for the call to finish, 0 to 50. A call that finishes in time is answered with its result. One still running is answered with `status: "pending"`: it goes on running, and reading the call again by its `id` returns its result. 0 answers at once. From 0 to 50. |

## Response

### 200

The call, finished: succeeded, or an earlier call replayed or reused.

The body is a call: see [the call response](/docs/call-response.md) for every field, an example and the error bodies.

- `Idempotent-Replayed`: `true` when the response is an earlier call returned for its `Idempotency-Key`. Absent otherwise.

### 202

The call is still running after `wait` seconds: it has `status: "pending"` and no `output` or `cost` yet. It goes on running; read it at `GET /v1/calls/{call_id}`, with `wait` to wait for it.

The body is a call: see [the call response](/docs/call-response.md) for every field, an example and the error bodies.

- `Location`: Where to read the call.
- `Retry-After`: Seconds to wait before reading it without `wait`.
- `Idempotent-Replayed`: `true` when the response is an earlier call returned for its `Idempotency-Key`. Absent otherwise.

### Example 200 response

```json
{
  "id": "tcall_8xQ2vT7mKpR4nW9cYb3LdE",
  "tool": "google.serp.organic",
  "status": "succeeded",
  "failure_reason": null,
  "provider": "example-provider",
  "requested_provider": null,
  "input": {
    "query": "crm for small teams",
    "country": "gb"
  },
  "resolved_input": {
    "query": "crm for small teams",
    "country": "gb",
    "language": "en",
    "device": "desktop",
    "page": 1,
    "autocorrect": true
  },
  "require": [],
  "cost": {
    "provider_usd": "0.02",
    "platform_fee_usd": "0.0014",
    "total_usd": "0.0214"
  },
  "label": "weekly-rankings",
  "created_at": "2026-01-05T09:00:00Z",
  "completed_at": "2026-01-05T09:00:02Z",
  "output": {
    "results": [
      {
        "position": 1,
        "title": "5 best CRMs for small teams (and how to choose the right ...",
        "url": "https://www.insightly.com/blog/top-crms-for-small-teams/",
        "snippet": "Insightly is a modern, affordable CRM that small teams love because it's easy to use, you can customize it to your needs, and it scales with you as you grow. Read more",
        "displayed_url": "https://www.insightly.com › blog › top-crms-for-small-t..."
      },
      {
        "position": 2,
        "title": "Best CRM software for small businesses (2026)",
        "url": "https://www.enterprisenation.com/learn-something/best-crm-software-small-businesses/",
        "snippet": "22 Dec 2025 — Comprehensive set of features: HubSpot CRM lets you manage your contacts and track leads with ease. You can also integrate your email marketing. Read more",
        "displayed_url": "https://www.enterprisenation.com › learn-something › b..."
      },
      {
        "position": 3,
        "title": "10 Shortlist UK's CRM Software for Small Business (2026)",
        "url": "https://www.dynamicssquare.co.uk/blog/best-crm-software-for-small-business/",
        "snippet": "12 Jul 2025 — CRM software provides teams with access to customer information, communication history, preferences, opportunities, and previous interactions. Read more",
        "displayed_url": "https://www.dynamicssquare.co.uk › blog › best-crm-sof..."
      }
    ],
    "related_questions": [],
    "related_searches": [
      {
        "query": "Best crm for small teams"
      },
      {
        "query": "Best CRM for small business free"
      },
      {
        "query": "Best CRM for small business reddit"
      }
    ]
  },
  "reused": false
}
```

### Example 202 response

```json
{
  "id": "tcall_8xQ2vT7mKpR4nW9cYb3LdE",
  "tool": "google.serp.organic",
  "status": "pending",
  "failure_reason": null,
  "provider": null,
  "requested_provider": null,
  "input": {
    "query": "crm for small teams",
    "country": "gb"
  },
  "resolved_input": {
    "query": "crm for small teams",
    "country": "gb",
    "language": "en",
    "device": "desktop",
    "page": 1,
    "autocorrect": true
  },
  "require": [],
  "cost": null,
  "label": "weekly-rankings",
  "created_at": "2026-01-05T09:00:00Z",
  "completed_at": null,
  "output": null,
  "reused": false
}
```

## Errors

| Status | Body | Description |
| --- | --- | --- |
| 400 | [Refused request](/docs/call-response.md#refused-request) | The request was refused. |
| 401 | [Refused request](/docs/call-response.md#refused-request) | The API key is missing, wrong or revoked (`INVALID_AUTH_TOKEN`). |
| 402 | [Refused request](/docs/call-response.md#refused-request) | Your available balance cannot cover the call (`INSUFFICIENT_BALANCE`). |
| 403 | [Refused request](/docs/call-response.md#refused-request) | The key's scope does not permit calls, the key is a test key, or a subscription is required. |
| 404 | [Refused request](/docs/call-response.md#refused-request) | No tool has this ID (`MODEL_NOT_FOUND`). |
| 409 | [Refused request](/docs/call-response.md#refused-request) | The tool cannot be called yet. |
| 422 | [Invalid request](/docs/call-response.md#invalid-request), [Refused request](/docs/call-response.md#refused-request) | The request or the idempotency key is not valid (`VALIDATION`, with `errors`), no provider can serve the request (`NO_ELIGIBLE_PROVIDER`), or the idempotency key was already used with a different request (`IDEMPOTENCY_KEY_REUSED`). |
| 429 | [Failed call](/docs/call-response.md#failed-call), [Refused request](/docs/call-response.md#refused-request) | Too many requests with this API key (`RATE_LIMITED`), or every provider that could serve the call is rate limiting requests (`PROVIDER_RATE_LIMITED`, with `call_id`; the call was not charged). |
| 502 | [Failed call](/docs/call-response.md#failed-call) | No provider served the call (`TOOL_CALL_FAILED`). Not charged, unless its provider billed the failure. |
| 503 | [Failed call](/docs/call-response.md#failed-call), [Refused request](/docs/call-response.md#refused-request) | The tool is not available right now, or the call could not be started: not charged, retry shortly. Or the call ran but its result could not be returned now (`CALL_RESULT_DEFERRED`, with `call_id` and a `Location` header): read it at `GET /v1/calls/{call_id}` instead of calling again. |
| 504 | [Failed call](/docs/call-response.md#failed-call) | The call ran out of time (`TOOL_CALL_TIMED_OUT`). Not charged. |
