# Tools

> What your agents can call while they reply — your server, signed, or the person's app over the WebSocket — each turned on per agent.

What your agents can call while they reply — your server, signed, or the person's app over the WebSocket — each turned on per agent. Reads on `agent`, writes on `agent:write`.

When and why to call these: the [tools guide](https://docs.lumavox.ai/agents/tools.md).

## List tools

`GET /v1/tools`

Your organisation's tool library, newest first, in one page: at most the platform's `limits.org.tools` (`GET /v1/platform/limits`).

A tool is something an agent's model may call while it replies: **`webhook`**, your server, sent a signed `POST` with the call's arguments; or **`app`**, the person's app, called over the agent's WebSocket. Each is turned on per agent (`PUT /v1/agents/{agent_id}/tools`), up to the platform's `limits.agent.tools` an agent (`GET /v1/platform/limits`).

```bash title="curl"
curl "https://api.lumavox.ai/v1/tools" \
  -H "Authorization: Bearer $API_KEY"
```

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — Every tool |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of ToolInfo |  |
| `data[].id` | string | The tool's id, `tl_…`. |
| `data[].name` | string | What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation; `search_knowledge` and `remember_relation` are the platform's own. |
| `data[].description` | string |  |
| `data[].kind` | `"webhook"`, `"app"` | `webhook`: your server, sent a signed `POST` with the call. `app`: the person's app, called over the agent's WebSocket. Fixed at creation. |
| `data[].parameters` | map of any | Its inputs, as a JSON Schema object: `{"type": "object", "properties": {…}, "required": […]}`, each property `string` (with an optional `enum`), `number`, `integer`, `boolean`, `array` (with `items`) or `object`, each with an optional `description`. Nothing else is taken: a key outside this set is refused rather than passed on unenforced. |
| `data[].url` | string or null | A webhook's address; `null` for an app's. |
| `data[].timeout_ms` | integer | How long a call may take, in milliseconds: 1000 to 30000, 10000 by default. Past it the call failed, and the model is told so. 1000–30000. |
| `data[].ask_first` | boolean | Never run on the model's word alone: the agent asks the person first, and the tool runs only after they say yes. |
| `data[].action_only` | boolean | An app's tool only: the app is told to act and the reply carries on at once, with no answer awaited. |
| `data[].filler` | string or null | The line said before the call; `null` for none. |
| `data[].header_names` | array of string | The names of the headers sent with each call; their values are never shown. |
| `data[].secret_hint` | string or null | The signing secret's last four characters; `null` for an app's tool. |
| `data[].agent_ids` | array of string | The agents it is turned on for. |
| `data[].version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the tool since you read it. |
| `data[].created_at` | string (date-time) |  |
| `data[].updated_at` | string (date-time) |  |
| `data[].recent_failures` | integer | Its failed calls in your agents' conversations within the platform's `agents.tools.failure_window_minutes` — a deadline missed, an error answered, a server out of reach — counted from the first; 0 once the window passes with none. Each conversation reports its failures at most once in the platform's `agents.tools.report_every_minutes`, so the count can lag by that much. At the platform's `agents.tools.failures_to_mail` in the window, your organisation's admins are mailed, at most once in its `agents.tools.mail_every_hours` per tool. Each is on `GET /v1/platform/limits`. |
| `data[].last_failed_at` | string (date-time) or null | When a call last failed; `null` if none has. |
| `data[].last_error` | string or null | What the model was told of the last failure; `null` if none has. |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Create a tool

`POST /v1/tools`

Needs `agent:write`.

A webhook tool answers its **signing secret** (`whsec_…`) this once: every call carries `X-Tool-Signature: t=<unix seconds>,v1=<hex>`, the HMAC-SHA256 of `<t>.<body>` under it. Check it, and that `t` is within five minutes. `headers` are sent with every call — your own API token — and stored encrypted.

**The tool is checked whole**: its `name` (what the model calls it by, unique in your organisation), its `parameters` (the JSON Schema subset the model takes), a webhook's `url` (`https://`, public, never this platform's) and its `headers` are each a 422 naming the problem.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/tools" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"lookup_order","description":"...","kind":"webhook"}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation; `search_knowledge` and `remember_relation` are the platform's own. |
| `description` | string | yes | What the tool does and when to call it, in the model's terms: it reads this to decide. 1–1000 characters. |
| `kind` | `"webhook"`, `"app"` | yes | `webhook`: your server, sent a signed `POST` with the call. `app`: the person's app, called over the agent's WebSocket. Fixed at creation. |
| `parameters` | map of any |  | Its inputs (as above); omitted: it takes none. |
| `url` | string or null |  | A webhook's `https://` address, public, and not this platform's. An app's tool has none. At most 2048 characters. |
| `timeout_ms` | integer |  | How long a call may take, in milliseconds: 1000 to 30000, 10000 by default. Past it the call failed, and the model is told so. 1000–30000. |
| `ask_first` | boolean |  | Never run on the model's word alone: the agent asks the person first, and the tool runs only after they say yes. |
| `action_only` | boolean |  | An app's tool only: the app is told to act and the reply carries on at once, with no answer awaited. |
| `filler` | string or null |  | A line the agent says before the call, while it waits: `Let me check that for you.` `null`: none. At most 200 characters. |
| `headers` | map of string or null |  | A webhook's own headers, sent with every call (your API token): at most 10, stored encrypted and never shown again — a tool answers their names. `Content-Type`, `User-Agent`, `X-Tool-*` and HTTP's own are the platform's. |

**Responses**

| Status | Description |
|---|---|
| `201` | `application/json` — The tool, with its signing secret shown this once |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `409` | A tool by that name exists already, or the library holds the platform's `limits.org.tools`. |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |
| `503` | This deployment cannot seal a tool's secret (`TOOL_SECRET_KEY` unset): webhook tools cannot be saved or called. |

**`201` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | The tool's id, `tl_…`. |
| `name` | string | What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation; `search_knowledge` and `remember_relation` are the platform's own. |
| `description` | string |  |
| `kind` | `"webhook"`, `"app"` | `webhook`: your server, sent a signed `POST` with the call. `app`: the person's app, called over the agent's WebSocket. Fixed at creation. |
| `parameters` | map of any | Its inputs, as a JSON Schema object: `{"type": "object", "properties": {…}, "required": […]}`, each property `string` (with an optional `enum`), `number`, `integer`, `boolean`, `array` (with `items`) or `object`, each with an optional `description`. Nothing else is taken: a key outside this set is refused rather than passed on unenforced. |
| `url` | string or null | A webhook's address; `null` for an app's. |
| `timeout_ms` | integer | How long a call may take, in milliseconds: 1000 to 30000, 10000 by default. Past it the call failed, and the model is told so. 1000–30000. |
| `ask_first` | boolean | Never run on the model's word alone: the agent asks the person first, and the tool runs only after they say yes. |
| `action_only` | boolean | An app's tool only: the app is told to act and the reply carries on at once, with no answer awaited. |
| `filler` | string or null | The line said before the call; `null` for none. |
| `header_names` | array of string | The names of the headers sent with each call; their values are never shown. |
| `secret_hint` | string or null | The signing secret's last four characters; `null` for an app's tool. |
| `agent_ids` | array of string | The agents it is turned on for. |
| `version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the tool since you read it. |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |
| `recent_failures` | integer | Its failed calls in your agents' conversations within the platform's `agents.tools.failure_window_minutes` — a deadline missed, an error answered, a server out of reach — counted from the first; 0 once the window passes with none. Each conversation reports its failures at most once in the platform's `agents.tools.report_every_minutes`, so the count can lag by that much. At the platform's `agents.tools.failures_to_mail` in the window, your organisation's admins are mailed, at most once in its `agents.tools.mail_every_hours` per tool. Each is on `GET /v1/platform/limits`. |
| `last_failed_at` | string (date-time) or null | When a call last failed; `null` if none has. |
| `last_error` | string or null | What the model was told of the last failure; `null` if none has. |
| `secret` | string or null | The signing secret, `whsec_…`, **shown this once**: every call is signed with it (`X-Tool-Signature`). Keep it on your server. `null` for an app's tool. |

## Read a tool

`GET /v1/tools/{tool_id}`

```bash title="curl"
curl "https://api.lumavox.ai/v1/tools/$TOOL_ID" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `tool_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The tool |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown tool id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | The tool's id, `tl_…`. |
| `name` | string | What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation; `search_knowledge` and `remember_relation` are the platform's own. |
| `description` | string |  |
| `kind` | `"webhook"`, `"app"` | `webhook`: your server, sent a signed `POST` with the call. `app`: the person's app, called over the agent's WebSocket. Fixed at creation. |
| `parameters` | map of any | Its inputs, as a JSON Schema object: `{"type": "object", "properties": {…}, "required": […]}`, each property `string` (with an optional `enum`), `number`, `integer`, `boolean`, `array` (with `items`) or `object`, each with an optional `description`. Nothing else is taken: a key outside this set is refused rather than passed on unenforced. |
| `url` | string or null | A webhook's address; `null` for an app's. |
| `timeout_ms` | integer | How long a call may take, in milliseconds: 1000 to 30000, 10000 by default. Past it the call failed, and the model is told so. 1000–30000. |
| `ask_first` | boolean | Never run on the model's word alone: the agent asks the person first, and the tool runs only after they say yes. |
| `action_only` | boolean | An app's tool only: the app is told to act and the reply carries on at once, with no answer awaited. |
| `filler` | string or null | The line said before the call; `null` for none. |
| `header_names` | array of string | The names of the headers sent with each call; their values are never shown. |
| `secret_hint` | string or null | The signing secret's last four characters; `null` for an app's tool. |
| `agent_ids` | array of string | The agents it is turned on for. |
| `version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the tool since you read it. |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |
| `recent_failures` | integer | Its failed calls in your agents' conversations within the platform's `agents.tools.failure_window_minutes` — a deadline missed, an error answered, a server out of reach — counted from the first; 0 once the window passes with none. Each conversation reports its failures at most once in the platform's `agents.tools.report_every_minutes`, so the count can lag by that much. At the platform's `agents.tools.failures_to_mail` in the window, your organisation's admins are mailed, at most once in its `agents.tools.mail_every_hours` per tool. Each is on `GET /v1/platform/limits`. |
| `last_failed_at` | string (date-time) or null | When a call last failed; `null` if none has. |
| `last_error` | string or null | What the model was told of the last failure; `null` if none has. |

## Update a tool

`PATCH /v1/tools/{tool_id}`

Needs `agent:write`.

A partial update: omitted fields are left alone, an explicit `null` clears one, and `headers` replaces them all. `kind` is fixed.

**Conversations already open pick this up within about a minute**, at their next turn after the change reaches them. Send `expected_version` to be refused with 409 if somebody else changed the tool since you read it.

```bash title="curl"
curl -X PATCH "https://api.lumavox.ai/v1/tools/$TOOL_ID" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `tool_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  | What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation; `search_knowledge` and `remember_relation` are the platform's own. |
| `description` | string |  | What the tool does and when to call it, in the model's terms: it reads this to decide. 1–1000 characters. |
| `parameters` | map of any |  | Its inputs, as a JSON Schema object: `{"type": "object", "properties": {…}, "required": […]}`, each property `string` (with an optional `enum`), `number`, `integer`, `boolean`, `array` (with `items`) or `object`, each with an optional `description`. Nothing else is taken: a key outside this set is refused rather than passed on unenforced. |
| `url` | string |  | A webhook's `https://` address, public, and not this platform's. An app's tool has none. At most 2048 characters. |
| `timeout_ms` | integer |  | How long a call may take, in milliseconds: 1000 to 30000, 10000 by default. Past it the call failed, and the model is told so. 1000–30000. |
| `ask_first` | boolean |  | Never run on the model's word alone: the agent asks the person first, and the tool runs only after they say yes. |
| `action_only` | boolean |  | An app's tool only: the app is told to act and the reply carries on at once, with no answer awaited. |
| `filler` | string or null |  | A line the agent says before the call, while it waits: `Let me check that for you.` `null`: none. At most 200 characters. |
| `headers` | map of string or null |  | Replaces every header; `null` or `{}` sends none. |
| `expected_version` | integer or null |  | The `version` you last read. When set and the tool has been changed since, the update is refused with 409 and nothing is written. Omit it to write unconditionally. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The tool as it now stands |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown tool id |
| `409` | `expected_version` was sent and the tool has been changed since, or the new name is taken. Nothing was written. |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |
| `503` | This deployment cannot seal a tool's secret (`TOOL_SECRET_KEY` unset): webhook tools cannot be saved or called. |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | The tool's id, `tl_…`. |
| `name` | string | What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation; `search_knowledge` and `remember_relation` are the platform's own. |
| `description` | string |  |
| `kind` | `"webhook"`, `"app"` | `webhook`: your server, sent a signed `POST` with the call. `app`: the person's app, called over the agent's WebSocket. Fixed at creation. |
| `parameters` | map of any | Its inputs, as a JSON Schema object: `{"type": "object", "properties": {…}, "required": […]}`, each property `string` (with an optional `enum`), `number`, `integer`, `boolean`, `array` (with `items`) or `object`, each with an optional `description`. Nothing else is taken: a key outside this set is refused rather than passed on unenforced. |
| `url` | string or null | A webhook's address; `null` for an app's. |
| `timeout_ms` | integer | How long a call may take, in milliseconds: 1000 to 30000, 10000 by default. Past it the call failed, and the model is told so. 1000–30000. |
| `ask_first` | boolean | Never run on the model's word alone: the agent asks the person first, and the tool runs only after they say yes. |
| `action_only` | boolean | An app's tool only: the app is told to act and the reply carries on at once, with no answer awaited. |
| `filler` | string or null | The line said before the call; `null` for none. |
| `header_names` | array of string | The names of the headers sent with each call; their values are never shown. |
| `secret_hint` | string or null | The signing secret's last four characters; `null` for an app's tool. |
| `agent_ids` | array of string | The agents it is turned on for. |
| `version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the tool since you read it. |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |
| `recent_failures` | integer | Its failed calls in your agents' conversations within the platform's `agents.tools.failure_window_minutes` — a deadline missed, an error answered, a server out of reach — counted from the first; 0 once the window passes with none. Each conversation reports its failures at most once in the platform's `agents.tools.report_every_minutes`, so the count can lag by that much. At the platform's `agents.tools.failures_to_mail` in the window, your organisation's admins are mailed, at most once in its `agents.tools.mail_every_hours` per tool. Each is on `GET /v1/platform/limits`. |
| `last_failed_at` | string (date-time) or null | When a call last failed; `null` if none has. |
| `last_error` | string or null | What the model was told of the last failure; `null` if none has. |

## Delete a tool

`DELETE /v1/tools/{tool_id}`

Needs `agent:write`.

Turns it off for every agent that had it; conversations already open stop offering it within about a minute. This cannot be undone.

```bash title="curl"
curl -X DELETE "https://api.lumavox.ai/v1/tools/$TOOL_ID" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `tool_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `204` | The tool is gone |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown tool id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

## A tool's recent calls

`GET /v1/tools/{tool_id}/calls`

Its most recent calls in your agents' conversations, newest first, from the past seven days: when, in which conversation and for whom, how long each took, and what failed. **No inputs or answers**: those are each turn's, in the conversation's history (`tool_calls`). A call shows here within about a minute. **Sampled at volume**: a tool called thousands of times an hour keeps a share of its calls here, never all — never a count to bill on. `next_cursor` is always `null`: ask for up to 100 with `limit`. Answers 503 while this deployment cannot read call logs.

```bash title="curl"
curl "https://api.lumavox.ai/v1/tools/$TOOL_ID/calls" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `tool_id` | string | yes |  |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer |  | How many of the most recent calls, 1 to 100. 1–100. Default `50`. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — Its most recent calls, newest first |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown tool id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |
| `502` | The call log did not answer |
| `503` | This deployment cannot read call logs yet (`CF_ACCOUNT_ID`, and the admin panel's analytics token) |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of ToolCall |  |
| `data[].at` | string (date-time) | When the call ended. |
| `data[].call_id` | string | The call's id: the `call_id` your server was sent, and the turn's `tool_calls` carry. |
| `data[].agent_id` | string |  |
| `data[].session_id` | string | The conversation it was called in. |
| `data[].end_user_id` | string | The person, `eu_…`, whose conversation it was. |
| `data[].ok` | boolean | It answered in time, and not with an error. |
| `data[].duration_ms` | integer |  |
| `data[].error` | string or null | What the model was told failed; `null` when nothing did. |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Rotate a tool's signing secret

`POST /v1/tools/{tool_id}/secret`

Needs `agent:write`.

A new signing secret, **shown this once**; the old one stops signing within about a minute, as conversations pick the change up. A webhook tool only.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/tools/$TOOL_ID/secret" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `tool_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The tool, with its new signing secret |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown tool id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |
| `503` | This deployment cannot seal a tool's secret (`TOOL_SECRET_KEY` unset): webhook tools cannot be saved or called. |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | The tool's id, `tl_…`. |
| `name` | string | What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation; `search_knowledge` and `remember_relation` are the platform's own. |
| `description` | string |  |
| `kind` | `"webhook"`, `"app"` | `webhook`: your server, sent a signed `POST` with the call. `app`: the person's app, called over the agent's WebSocket. Fixed at creation. |
| `parameters` | map of any | Its inputs, as a JSON Schema object: `{"type": "object", "properties": {…}, "required": […]}`, each property `string` (with an optional `enum`), `number`, `integer`, `boolean`, `array` (with `items`) or `object`, each with an optional `description`. Nothing else is taken: a key outside this set is refused rather than passed on unenforced. |
| `url` | string or null | A webhook's address; `null` for an app's. |
| `timeout_ms` | integer | How long a call may take, in milliseconds: 1000 to 30000, 10000 by default. Past it the call failed, and the model is told so. 1000–30000. |
| `ask_first` | boolean | Never run on the model's word alone: the agent asks the person first, and the tool runs only after they say yes. |
| `action_only` | boolean | An app's tool only: the app is told to act and the reply carries on at once, with no answer awaited. |
| `filler` | string or null | The line said before the call; `null` for none. |
| `header_names` | array of string | The names of the headers sent with each call; their values are never shown. |
| `secret_hint` | string or null | The signing secret's last four characters; `null` for an app's tool. |
| `agent_ids` | array of string | The agents it is turned on for. |
| `version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the tool since you read it. |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |
| `recent_failures` | integer | Its failed calls in your agents' conversations within the platform's `agents.tools.failure_window_minutes` — a deadline missed, an error answered, a server out of reach — counted from the first; 0 once the window passes with none. Each conversation reports its failures at most once in the platform's `agents.tools.report_every_minutes`, so the count can lag by that much. At the platform's `agents.tools.failures_to_mail` in the window, your organisation's admins are mailed, at most once in its `agents.tools.mail_every_hours` per tool. Each is on `GET /v1/platform/limits`. |
| `last_failed_at` | string (date-time) or null | When a call last failed; `null` if none has. |
| `last_error` | string or null | What the model was told of the last failure; `null` if none has. |
| `secret` | string or null | The signing secret, `whsec_…`, **shown this once**: every call is signed with it (`X-Tool-Signature`). Keep it on your server. `null` for an app's tool. |

## Call a tool once, as a test

`POST /v1/tools/{tool_id}/test`

Needs `agent:write`.

Sends your server one call, exactly as a conversation would — signed, with your headers, under the tool's timeout — carrying the `arguments` you give and `test: true`, with no agent, conversation or person. Answers what came back as the model would read it: the JSON, or the text, cut at the platform's `agents.tools.result_max_bytes` (`GET /v1/platform/limits`), or what failed. A webhook tool only; nothing is charged.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/tools/$TOOL_ID/test" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `tool_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `arguments` | map of any |  | What the call carries as `arguments`; `{}` when omitted. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — What your server answered, or why it did not |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown tool id |
| `409` | The tool's stored secret cannot be opened: `TOOL_SECRET_KEY` changed since it was saved. Rotate its secret, and save its headers again. |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |
| `503` | This deployment cannot seal a tool's secret (`TOOL_SECRET_KEY` unset): webhook tools cannot be saved or called. |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `ok` | boolean | Your server answered 2xx within the timeout. |
| `status` | integer or null | Its HTTP status; `null` when none came (unreachable, or too slow). |
| `duration_ms` | integer | From sending to its answer's last byte. |
| `result` | any | Its answer: the JSON it sent, else its text, cut at the platform's `agents.tools.result_max_bytes` (`GET /v1/platform/limits`); `null` for none. |
| `truncated` | boolean | The answer was longer than the platform's `agents.tools.result_max_bytes` and was cut. |
| `error` | string or null | What failed, as the model would be told; `null` when it did not. |
