> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upliftai.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook tools

A webhook tool is a function **your server** runs when the agent asks. Mid-call, we POST the tool call to your HTTPS endpoint, you answer in the same request, and the agent speaks the JSON you return. Phone calls and web sessions alike. Use them for anything that lives in your systems: order status, account balance, stock, a new ticket.

## How it fits together

```mermaid theme={null}
sequenceDiagram
    participant Caller
    participant Agent
    participant Uplift as Uplift AI runtime
    participant You as Your endpoint
    Caller->>Agent: "Where is my order?"
    Note over Agent: model picks get_order_status
    Agent->>Uplift: tool call
    Uplift->>You: POST https://example.com/uplift/tools<br/>{ "tool": "get_order_status", "args": { "orderNumber": "4471" } }
    Note over You: your server, an n8n workflow,<br/>anything that answers with JSON
    You-->>Uplift: { "status": "shipped", "eta": "Thursday" }
    Uplift-->>Agent: result
    Agent->>Caller: "It has shipped, it will reach you Thursday"
```

The model decides to call the tool. Our runtime does the rest. You have ten seconds for the answer.

## Set it up

<Steps>
  <Step title="Tell us the URL to call">
    That is a **tool endpoint connection**. The URL can be your own server, an n8n workflow with a Webhook trigger, or anything else that answers an HTTPS POST with JSON. On n8n, set the Webhook node to respond when the workflow finishes, not immediately. Add it on the portal's [Webhooks page](https://upliftai.org/app/calling/webhooks): a **name**, the **HTTPS URL**, and a **signing secret**. **Generate** makes a strong secret for you. Store it on your server. Every request is signed with it.

    Keep the new connection's id. A tool endpoint only ever receives tool calls and the health check, never [call events](/voice-agents/webhooks/overview). Those go to a webhook, and a tool pointed at one fails. I recommend a route of its own for tool calls, so the two never share a handler.
  </Step>

  <Step title="Add the tool to your assistant">
    It goes in `config.agent.tools`, next to the prompt that will use it. The `execution` block is what makes it a webhook tool:

    ```json theme={null}
    {
      "name": "Order desk",
      "config": {
        "agent": {
          "instructions": "You confirm orders for our shop. When the caller asks where their order is, look it up and read back the status and the delivery day.",
          "tools": [
            {
              "name": "get_order_status",
              "description": "Look up an order by its number. Call this when the caller asks where their order is.",
              "parameters": {
                "type": "object",
                "properties": {
                  "orderNumber": { "type": "string", "description": "The order number the caller read out" }
                },
                "required": ["orderNumber"]
              },
              "execution": { "type": "tool_endpoint", "connectionId": "01efae24-b353-4621-a85a-4a04cba97570" }
            }
          ]
        },
        "stt": { ... },
        "tts": { ... },
        "llm": { ... }
      }
    }
    ```

    That is a [create assistant](/api-reference/assistants/create-an-assistant) body. On the portal you can create the same tool once under Settings → Tools and assign it to any assistant.
  </Step>

  <Step title="Tell the prompt to say it's checking">
    Silence while the tool runs reads as a dropped line. One line in the prompt fixes it:

    ```
    Before looking up an order, say you are checking the system.
    ```
  </Step>
</Steps>

<Note>
  On web sessions, only tools assigned from the portal's tool registry reach your endpoint today. A tool written inline on the assistant config runs on phone calls and campaigns. On a web session the model can still pick it and get an error result.
</Note>

## The request

```http theme={null}
POST /uplift/tools HTTP/1.1
Host: example.com
Content-Type: application/json
x-uplift-ai-signature: t=1755772800,v1=6c0f0e8b1a…

{ "tool": "get_order_status", "args": { "orderNumber": "4471" } }
```

* **Signed like event webhooks.** Same header, same HMAC over `${t}.${body}` with the connection's `signingSecret`. Verify it the way the [webhooks page](/voice-agents/webhooks/overview#verifying-the-signature) shows.
* **Only `tool` and `args`.** The body carries no call id, no phone number, no assistant id. If the handler needs to know who is on the line, make it an argument and give the model the value up front as a [variable](/voice-agents/personalization/variables).

## The response

Any 2xx with a JSON body. The body lands in the conversation as the tool result, and the model writes its next sentence from it. Write for that reader: smart, in a hurry, about to speak out loud.

```json theme={null}
{ "status": "shipped", "eta": "Thursday", "courier": "TCS" }
```

* **Respond within ten seconds.** The budget runs from our connect to your response headers. The `timeout` on a tool definition is for client tools and does not stretch this.
* **Answer the question, not the schema.** Return the two or three fields the next sentence needs. A big body costs tokens and time on every turn that follows.
* **Lead with what happened.** A `status` the model can act on: `shipped`, `not_found`, `already_booked`. For an action with nothing to report, `{ "status": "done" }` is enough.
* **Make values speakable.** "Thursday", not an ISO timestamp. "5,999 rupees", not `599900`. The model says it the way you wrote it.
* **Treat errors as results.** Return `{ "status": "error", "message": "no order with that number" }` with a 2xx, and the model tells the caller in its own words. A 4xx or 5xx only tells it the tool failed.
* **Use names a person would quote.** The order number, yes. Database keys and internal codes, no. The model handles words far better than opaque ids.
* **Steer the wording when it matters.** A `say` field is a hint the model usually follows: `{ "status": "pending", "say": "someone will call you back with that" }`.

OpenAI and Anthropic give the same advice. See the [function calling guide](https://developers.openai.com/api/docs/guides/function-calling) and [Writing tools for agents](https://www.anthropic.com/engineering/writing-tools-for-agents).

A non-2xx, a body that isn't JSON, or a timeout is an error result. The model hears that the tool failed and keeps talking. Your error body is logged on our side for debugging, never spoken. There are no retries. A tool call is one synchronous round trip, unlike [event deliveries](/voice-agents/webhooks/overview#delivery-and-retries).

## Handle it on your server

A Node handler that verifies, branches, and answers inside the budget:

```js theme={null}
import express from 'express'
import { verify } from './uplift-signature' // the verify function from the webhooks page

const app = express()
const SECRET = process.env.UPLIFT_SIGNING_SECRET

app.post('/uplift/tools', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verify(req.body, req.get('x-uplift-ai-signature'), SECRET)) return res.sendStatus(401)
  const body = JSON.parse(req.body)

  if (body.type === 'ping') return res.sendStatus(200)

  if (body.tool === 'get_order_status') {
    const order = await orders.byNumber(body.args.orderNumber)
    if (!order) return res.json({ status: 'not_found' })
    return res.json({ status: order.status, eta: order.eta, courier: order.courier })
  }
  return res.status(404).json({ error: `unknown tool ${body.tool}` })
})
```

Sign over the **raw body**, before any JSON parsing touches it.

## Testing from localhost

We only call **public HTTPS** URLs. `localhost` and private addresses are rejected when you save the connection, so put a tunnel in front of your dev server:

```bash theme={null}
ngrok http 3000
```

The tunnel prints a public address like `https://a1b2c3d4.ngrok-free.app`. The `url` is that address plus your route. Add a tool endpoint with it on the [Webhooks page](https://upliftai.org/app/calling/webhooks), exactly as in [Set it up](#set-it-up), with the secret your local server checks. Put the new connection's id in the tool's `execution`, so the agent calls your tunnel instead of the production endpoint:

```json theme={null}
"execution": { "type": "tool_endpoint", "connectionId": "7f3c2a10-4b8e-4d21-9c55-1e2f3a4b5c6d" }
```

Then make a call and ask for something the tool answers.

Every request shows up in ngrok's inspector at `http://127.0.0.1:4040`, headers and body included. Replay one from there while you fix the handler.

* **Free ngrok URLs change every restart.** Claim ngrok's free static domain, so the URL you saved keeps working.
* **The tunnel is public.** Keep the signature check on, even on your laptop.

## Before you ship

* **Cache what you can.** A cache or a fast index answers inside the budget. If the real answer takes longer, return a `pending` status and do the work on your side.
* **Campaign calls carry the same tools.** A webhook tool on the assistant runs on every campaign call that assistant makes, next to the goal's system tools.
* **Debug from the [session detail](/api-reference/sessions-%26-call-records/get-a-calls-transcript-and-outcomes).** It lists every tool call afterwards, arguments and result included.
