> For the full page index, fetch https://echonova.ai/llms.txt

# Webhook tools

Give an agent one HTTP endpoint of your own to call mid-conversation, with the parameters the model fills in and the credentials the portal injects.

A webhook tool is one HTTP endpoint of yours, described once and handed to the
agent. You give it a name, a description the agent reads, a URL and a list of
parameters. When the agent decides to call it, the request arrives at your endpoint
with your credentials already on it, and the agent reads your reply and keeps
talking. The builder puts it in a line: "A server tool lets the agent call an
external API mid-conversation."

## When to use this kind

- One endpoint with a handful of parameters, where you want to write the request
  yourself. That is a webhook tool.
- A whole API with many operations. Import the document instead as an
  [OpenAPI tool](/docs/agents/openapi-tools), rather than hand building one webhook
  tool per operation.
- An action inside a product you have already connected, such as posting a Slack
  reaction or creating a calendar event. Use an integration tool, which takes its
  operation and its credentials from the integration.

Webhook tools live in the workspace tool library and are given to an agent by
selection, the same way every workspace tool is. See
[Tools overview](/docs/agents/tools-overview) for the difference between the library
and a per-agent selection.

## What you need

- A public endpoint reachable over HTTPS. Loopback, private and link-local
  addresses are rejected, so a service on your own network will not work.
- Membership of the workspace. Creating a tool needs no further permission.
- Any credential the endpoint requires, saved first as a
  [secret](/docs/agents/secrets) or an
  [auth connection](/docs/agents/auth-connections).

## Define

Open an agent, go to the **Tools** tab and press **Add Tool**, then the **Webhook**
tab of the dialog. The same dialog is on the **Tool Library** page. Editing an
existing tool opens a wider sheet titled **Edit tool**, which carries two controls
the create dialog does not: body parameters and a per-parameter **Required** toggle.

### Name

Required, and unique in the workspace. The placeholder suggests the shape the agent
works best with, `e.g. lookup_order`. Leading and trailing spaces are trimmed. A
name already taken is refused with
`A tool named "<name>" already exists in this workspace`.

### Description

This is the text the agent reads to decide whether to call the tool, which is why
the box prompts with "What does this tool do? The agent uses this to decide when to
call it." The **Add Tool** dialog caps it at 500 characters and shows a counter.
Write the cases you want the tool used for, not a restatement of the name.

### Method

One of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. It is kept in upper case whatever
you type.

### URL

Required, and it must be a public HTTPS URL. Path parameters appear as `{curly}`
placeholders, as the hint says: "A public https URL. Wrap path parameters in curly
braces." A URL that fails the check is rejected with
`url must be a public HTTPS URL (no loopback, private, or link-local addresses)`.

Placeholders and path parameters have to line up exactly, in both directions. A
placeholder with no parameter behind it fails with
`URL placeholder {orderId} has no matching path parameter`, and a path parameter
that never appears in the URL fails with
`Path parameter "orderId" does not appear in the URL`.

### Timeout [ms]

Optional. The field starts at `10000`. Leave it empty and the call still gives up
after 10000 ms. Anything above 30000 ms is brought down to 30000 ms when the call is
made.

### Parameters

Parameters are grouped by where they land on the request. The **Add Tool** dialog
offers three groups:

| Group                | What it does                                                |
| -------------------- | ----------------------------------------------------------- |
| **Headers**          | Sent as HTTP headers. The only group that may hold a secret |
| **Path parameters**  | Bound to a `{placeholder}` in the URL path, URL encoded     |
| **Query parameters** | Appended to the URL as `?key=value`                         |

The **Edit tool** sheet adds a fourth, **Body parameters**, described there as
"Fields sent in the JSON request body (POST/PUT/PATCH only)". Adding one under `GET`
or `DELETE` is refused with `GET requests cannot have body parameters`.

Each parameter carries:

| Field           | Values                                     | Notes                                                                                     |
| --------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------- |
| **Name**        | Any non-empty string                       | Blank fails with `Parameter name is required`                                             |
| **Value**       | **AI generated**, **Constant**, **Secret** | **Secret** appears on header rows only                                                    |
| **Data type**   | **String**, **Number**, **Boolean**        | Decides how the value is coerced and what the agent is asked for                          |
| **Description** | Free text                                  | The agent reads this for **AI generated** values                                          |
| **Required**    | On or off                                  | Only on the **Edit tool** sheet. New rows in the **Add Tool** dialog are created required |

Two parameters with the same name in the same group are refused with
`Duplicate query parameter "locale"`. A constant with no value is refused with
`Constant parameter "locale" needs a value`. A secret outside the headers group is
refused with `Secret parameter "X-Api-Key" is only allowed in headers`, and one with
no secret chosen with `Secret parameter "X-Api-Key" needs a secretId`.

An empty value is dropped rather than sent: a parameter whose value comes out
`undefined`, `null` or an empty string is skipped, so the placeholder stays in the
URL and the key never appears in the query string or the body.

## Get it right

Two pieces of text decide whether this tool works: the description the agent reads,
and the reply your endpoint sends back.

The description has to say when to call the tool and what comes back, because it is
all the agent gets.

**Recommended**

```text
Look up a customer's order by its order number. Call this when the caller asks
where an order is, when it will arrive, or what it contained. Returns the status,
the carrier and the expected delivery date.
```

**Less effective**

```text
Order lookup. Calls the orders API.
```

Your reply is read out loud in a conversation, so return the few fields the agent
needs and name them plainly. A full record dump makes the agent slower and can push
the response past the 256 KB ceiling.

**Recommended**

```json
{ "status": "shipped", "carrier": "Aramex", "expectedDelivery": "2026-09-18" }
```

**Less effective**

```json
{ "ok": true, "data": { "order": { "id": 4512, "lines": [], "customer": {}, "audit": [] } } }
```

## Auth

Authentication is a separate step from parameters, and there are two routes.

**Auth connection.** Pick one saved workspace connection under **Auth connection**,
or leave it at **No authentication**. Four kinds exist: basic, bearer, OAuth2 client
credentials and OAuth2 JWT. All four arrive as an `Authorization` header, so the
endpoint sees `Basic …` or `Bearer …`. The connection's header wins any header
parameter of the same name, because auth is written last. A connection that no
longer exists in the workspace is refused at save with
`Referenced credential not found`.

**Secret header parameter.** Add a header parameter, set its **Value** to **Secret**
and choose a workspace secret. The value is read when the call is made, never kept
on the tool and never shown to the agent. A secret that no longer exists is refused
at save with `Referenced secret not found`. If a secret cannot be read when the call
is made, the header is left out and the request still goes out.

When an OAuth2 connection is in play and your endpoint answers `401`, EchoNova
fetches a fresh token and sends the request once more. That retry happens for `401`
only, and only once.

> [!WARNING]
> Requests to your endpoint carry no EchoNova signature and no shared secret unless
> you add one yourself. Treat the endpoint as publicly reachable: require a
> credential through an auth connection or a secret header parameter, and reject
> every call that does not carry it.

## The request your endpoint receives

Take a tool with method `POST`, URL `https://api.example.com/orders/{orderId}`, a
path parameter `orderId` filled by the agent, a constant query parameter `locale`, a
secret header `X-Api-Key` and a body parameter `reason` filled by the agent. The
request that arrives looks like this:

```http
POST /orders/4512?locale=en HTTP/1.1
Host: api.example.com
X-Api-Key: <value of the workspace secret>
Content-Type: application/json

{"reason":"caller asked for a refund"}
```

Three things are worth reading off that request. `Content-Type: application/json` is
set only when the method allows a body and at least one body parameter came out with
a value, and it never overwrites a `Content-Type` header you defined yourself. Path
values are URL encoded. Redirects are not followed, so a `301` or `302` comes back
as that status rather than as the page behind it.

Replay the same request from your own machine to confirm the endpoint, the
credential and the response shape before you give the tool to an agent.

:::tabs

```bash
curl -X POST "https://api.example.com/orders/4512?locale=en" \
  -H "X-Api-Key: $MY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"caller asked for a refund"}'
```

```python
import os
import requests

response = requests.post(
    "https://api.example.com/orders/4512",
    params={"locale": "en"},
    headers={"X-Api-Key": os.environ["MY_API_KEY"]},
    json={"reason": "caller asked for a refund"},
    timeout=10,
)
print(response.status_code, response.text[:500])
```

```javascript
const response = await fetch('https://api.example.com/orders/4512?locale=en', {
  method: 'POST',
  headers: {
    'X-Api-Key': process.env.MY_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ reason: 'caller asked for a refund' }),
});
console.log(response.status, (await response.text()).slice(0, 500));
```

:::

## What the agent sees

The agent receives the tool's name, its description and a JSON schema built from the
parameters whose **Value** is **AI generated**. Constants and secrets are left out of
that schema entirely, so the agent cannot see them, cannot change them and cannot be
talked into revealing them. A tool with no **AI generated** parameter reaches the
agent with no parameters at all, which the builder states plainly: "No parameters.
The agent will call the endpoint as-is."

Each schema property carries the parameter's data type and, when you wrote one, its
**Description**, which is the sentence the agent uses to work out what to put there.
Parameters marked required are listed as required.

A selected webhook tool is in front of the agent on every turn. Nothing is searched
and nothing is ranked, so when a webhook tool never fires, the description is where
to look.

Your reply comes back to the agent as a status, an ok flag and a body. A body is
read as JSON when your response carries `application/json`, and passed through as
text otherwise.

## Check it before a caller does

A webhook tool has no run button in the portal, and it does not appear in
[the retrieval bench](/docs/agents/tool-bench), which searches imported OpenAPI
operations and database questions only. Verify one in two moves:

1. Replay the request yourself with the tabs above, to confirm the endpoint, the
   credential and the response shape.
2. Talk to the agent in the playground and say the thing that should trigger the
   call. See [Test your agent](/docs/agents/test-your-agent).

## Limits

| Limit             | Value                                 | Applies to                                                       |
| ----------------- | ------------------------------------- | ---------------------------------------------------------------- |
| Request timeout   | 10000 ms by default, 30000 ms maximum | Every call to your endpoint                                      |
| Response size     | 256 KB                                | Declared sizes above it are refused, larger bodies are truncated |
| Redirects         | Not followed                          | Returned as the redirect status                                  |
| URL scheme        | HTTPS only, public hosts only         | Save time and call time                                          |
| Secret parameters | Header group only                     | Save time                                                        |
| Body parameters   | `POST`, `PUT`, `PATCH` only           | Save time                                                        |
| Description       | 500 characters                        | The **Add Tool** dialog's box                                    |
| Tool name         | Unique inside the workspace           | Create and rename                                                |

## Troubleshooting

### The agent never calls the tool

The tool is in front of the agent on every turn, so this is a wording problem.
Check that the tool is selected on this agent, then rewrite the **Description** to
name the situations you want it used in. Do the same for each parameter's
**Description**, which is the only hint the agent gets about what a value should
contain.

### The call fails with "Tool request failed or timed out"

Your endpoint did not answer inside the timeout, or the connection failed. Raise
**Timeout [ms]**, up to the 30000 ms ceiling, and confirm the host resolves publicly
over HTTPS.

### The call fails with "Tool response too large"

Your endpoint declared a response above 256 KB. Return a smaller payload: the agent
has to read the result inside a conversation, so a trimmed object beats a full
record.

### Your endpoint sees no credential

Either no auth connection is picked, or the secret behind a header parameter could
not be read. An unreadable secret is left out and the request still goes out, so the
symptom is a missing header rather than a failed call. Re-pick the secret on the
parameter and save.

### A path placeholder is still in the URL

The value for that parameter came through empty, and an empty value is skipped,
which leaves its `{placeholder}` untouched. Mark the parameter required so the agent
has to fill it.

## FAQ

### Can the agent choose the URL?

No. The URL, the method and the placement of every parameter are fixed on the tool.
The agent only fills the values of parameters set to **AI generated**.

### What does my endpoint see the call coming from?

An ordinary HTTPS request from EchoNova, carrying only the parameters and
credentials you configured. Nothing on it identifies the caller or the conversation,
and nothing signs it, so your own credential check is the whole access control.

### Is this the same as a developer webhook?

No, and the names are the one confusing thing here. A webhook tool is an outbound
call the agent makes to your API during a conversation. A developer webhook is an
inbound delivery EchoNova sends you when a platform event happens. See
[Configure a webhook](/docs/developers/webhooks-configure) for that one.

### What happens when I delete the tool?

It is removed from the library and unselected from every agent that used it. The
portal states it before you confirm: "This deletes the tool and its associated
secret data, and deselects it from every agent that uses it. This action cannot be
undone." Auth connections
are reusable and are not deleted with the tool.

### Can I point a tool at a service inside my network?

No. The host must be public. Loopback, private ranges, link-local addresses and
carrier-grade NAT ranges are all blocked, on IPv4 and IPv6, at save time and again
at call time.

### Does removing a tool from one agent affect the others?

No. Removing it changes that agent's selection only. The library row and every other
agent stay as they are.

## Related

- [OpenAPI tools](/docs/agents/openapi-tools): import a whole API instead of one endpoint at a time
- [Workspace secrets](/docs/agents/secrets): store the API key this tool sends
- [Auth connections](/docs/agents/auth-connections): basic, bearer and OAuth2 credentials
- [Test your agent](/docs/agents/test-your-agent): trigger the call in the playground
- [Tools overview](/docs/agents/tools-overview): how the four kinds differ
- [Configure a webhook](/docs/developers/webhooks-configure): the inbound kind, which is a different feature
