# Webhook Tools

> Let your AI agent execute actions by calling your webhooks

## Webhook Tools

Tools allow your AI agent to execute actions by calling your webhooks. The LLM decides when to use a tool based on the conversation context, executes the call, and uses the result in its response.

> **Note:**
> **Tools run on every channel** — plain text, voice, and inside a flow's `tool`
> step. A text agent asked to look something up calls the tool and answers with
> the result; the model may chain up to 5 tool rounds per reply. Flows remain
> useful when you need deterministic sequencing rather than model-decided calls
> ([flows](https://www.zavu.dev/docs/guides/ai-agents/flows)).
>
> `npx zavudev agents test` warns you when an agent has tools its channels will never
> call.

> **Note:**
> **Before you build one:** `check_availability` and `book_meeting` already exist
> and Zavu runs them, so a booking agent needs no endpoint at all. See the
> [tool library](https://www.zavu.dev/docs/guides/ai-agents/tool-library). This page is for the rest, where
> the logic is yours.

## What are Agent Tools?

A **Tool** is a webhook endpoint that your agent can call to perform actions or retrieve data. When you define a tool, you specify:

- **Name**: How the agent refers to the tool
- **Description**: When the agent should use it
- **Parameters**: What data to send (JSON Schema)
- **Webhook URL**: Your endpoint that handles the request

The AI model uses the description to decide when to invoke the tool automatically.

## Use Cases

| Tool | Description | Example Use |
|------|-------------|-------------|
| `check_order_status` | Look up order tracking info | "Where is my order #12345?" |
| `create_support_ticket` | Open a support ticket | "I need help with my account" |
| `book_appointment` | Schedule a calendar event | "Book me for next Tuesday at 2pm" |
| `get_inventory` | Check product availability | "Do you have the blue shirt in medium?" |
| `lookup_customer` | Retrieve customer data | "What's my account balance?" |
| `process_refund` | Initiate a refund | "I want a refund for order #123" |

## Via Dashboard

  
#### Navigate to Tools

    Go to **Senders** > select your sender > **Agent** tab > **Tools** section.
  

  
#### Create New Tool

    Click **Create Tool** and fill in the form:

    - **Name**: A short identifier (e.g., `check_order_status`)
    - **Description**: When should the agent use this tool? Be specific.
    - **Webhook URL**: Your HTTPS endpoint
  

  
#### Define Parameters

    Specify the parameters your webhook expects:

    ```json
    {
      "type": "object",
      "properties": {
        "order_id": {
          "type": "string",
          "description": "The order ID to look up"
        }
      },
      "required": ["order_id"]
    }
    ```
  

  
#### Configure Security (Optional)

    Add a **Webhook Secret** to verify requests are from Zavu. We'll sign each request with this secret.
  

  
#### Test the Tool

    Use the **Test** button to send a sample request to your webhook and verify it responds correctly.
  

  
#### Enable the Tool

    Toggle the tool to **Enabled** so the agent can use it.
  

## Via API

### Create Tool

```typescript
import Zavudev from "@zavudev/sdk";

const zavu = new Zavudev({
  apiKey: process.env["ZAVUDEV_API_KEY"],
});

const tool = await zavu.senders.agent.tools.create("sender_abc123", {
  name: "check_order_status",
  description: "Look up the status and tracking information for a customer order. Use this when a customer asks about their order status, shipping, or delivery.",
  parameters: {
    type: "object",
    properties: {
      order_id: {
        type: "string",
        description: "The order ID (e.g., ORD-12345)",
      },
    },
    required: ["order_id"],
  },
  webhookUrl: "https://api.yourcompany.com/webhooks/order-status",
  webhookSecret: "whsec_your_secret_here", // Optional
});

console.log("Tool created:", tool.id);
```

```bash
curl -X POST https://api.zavu.dev/v1/senders/sender_abc123/agent/tools \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "check_order_status",
    "description": "Look up the status and tracking information for a customer order.",
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": {
          "type": "string",
          "description": "The order ID (e.g., ORD-12345)"
        }
      },
      "required": ["order_id"]
    },
    "webhookUrl": "https://api.yourcompany.com/webhooks/order-status",
    "webhookSecret": "whsec_your_secret_here"
  }'
```

### List Tools

```typescript
const tools = await zavu.senders.agent.tools.list("sender_abc123");

for (const tool of tools.items) {
  console.log(`${tool.name} - ${tool.enabled ? "Enabled" : "Disabled"}`);
}
```

```bash
curl https://api.zavu.dev/v1/senders/sender_abc123/agent/tools \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
```

### Update Tool

```typescript
const tool = await zavu.senders.agent.tools.update(
  "sender_abc123",
  "tool_xyz789",
  {
    description: "Updated description for better AI understanding",
    enabled: true,
  }
);
```

```bash
curl -X PATCH https://api.zavu.dev/v1/senders/sender_abc123/agent/tools/tool_xyz789 \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"description": "Updated description", "enabled": true}'
```

### Delete Tool

```typescript
await zavu.senders.agent.tools.delete("sender_abc123", "tool_xyz789");
```

```bash
curl -X DELETE https://api.zavu.dev/v1/senders/sender_abc123/agent/tools/tool_xyz789 \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
```

## Webhook Payload

When the agent invokes a tool, your webhook receives a POST request:

```json
{
  "tool": "check_order_status",
  "arguments": {
    "order_id": "ORD-12345"
  },
  "context": {
    "messageId": "msg_abc123",
    "contactPhone": "+14155551234",
    "sessionId": "session_xyz789"
  },
  "timestamp": 1703001234567
}
```

`context.contactPhone` is the number of the person writing, in a live
conversation. The model never sees it, so to look something up by who is
writing, read it from `context` instead of declaring it as a parameter. A dry
run (`POST /v1/agents/{agentId}/test` with `executeTools`) has no contact: its
`context` is `{"channel": "test"}`.

### Headers

| Header | Description |
|--------|-------------|
| `Content-Type` | `application/json` |
| `X-Zavu-Tool` | Tool name being invoked |
| `X-Zavu-Timestamp` | Unix timestamp of the request |
| `X-Zavu-Signature` | HMAC-SHA256 signature (if webhook secret is configured) |

## Webhook Response

Your webhook should return JSON with the result:

```json
{
  "status": "shipped",
  "carrier": "FedEx",
  "trackingNumber": "7891234567890",
  "estimatedDelivery": "2024-01-15"
}
```

The agent will incorporate this data into its response to the user.

### Error Responses

Return appropriate HTTP status codes for errors:

```json
{
  "error": "Order not found",
  "code": "ORDER_NOT_FOUND"
}
```

The agent will handle errors gracefully and inform the user appropriately.

## Signature Verification

If you configured a webhook secret, verify the signature to ensure requests are from Zavu:

```typescript
import crypto from "crypto";

function verifySignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your webhook handler. Sign the body exactly as it arrived: re-serializing
// parsed JSON can reorder keys or change spacing, and the signature then fails.
app.post("/webhooks/order-status", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-zavu-signature"];
  const payload = req.body.toString("utf8");

  if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const { arguments: args, context } = JSON.parse(payload);
  // Process the request...
});
```

```python
import hmac
import hashlib

def verify_signature(payload: str, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# In your webhook handler (Flask example)
@app.route("/webhooks/order-status", methods=["POST"])
def handle_order_status():
    signature = request.headers.get("X-Zavu-Signature")
    payload = request.get_data(as_text=True)

    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        return {"error": "Invalid signature"}, 401

    # Process the request...
```

```ruby
require "openssl"

def verify_signature(payload, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
  Rack::Utils.secure_compare(signature, expected)
end

# In your webhook handler (Sinatra example)
post "/webhooks/order-status" do
  signature = request.env["HTTP_X_ZAVU_SIGNATURE"]
  payload = request.body.read

  unless verify_signature(payload, signature, ENV["WEBHOOK_SECRET"])
    halt 401, { error: "Invalid signature" }.to_json
  end

  # Process the request...
end
```

```go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
)

func verifySignature(payload, signature, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(payload))
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(signature), []byte(expected))
}

// In your webhook handler
http.HandleFunc("/webhooks/order-status", func(w http.ResponseWriter, r *http.Request) {
	signature := r.Header.Get("X-Zavu-Signature")
	body, _ := io.ReadAll(r.Body)

	if !verifySignature(string(body), signature, os.Getenv("WEBHOOK_SECRET")) {
		http.Error(w, `{"error":"Invalid signature"}`, http.StatusUnauthorized)
		return
	}

	// Process the request...
})
```

```php
function verifySignature(string $payload, string $signature, string $secret): bool {
    $expected = hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

// In your webhook handler
$signature = $_SERVER['HTTP_X_ZAVU_SIGNATURE'] ?? '';
$payload = file_get_contents('php://input');

if (!verifySignature($payload, $signature, getenv('WEBHOOK_SECRET'))) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Process the request...
```

## Tool Execution Flow

```
Customer: "Where is my order #12345?"
                    |
                    v
            +---------------+
            |   AI Agent    |
            | Analyzes msg  |
            +---------------+
                    |
                    v
            +---------------+
            | Decides to    |
            | use tool      |
            +---------------+
                    |
                    v
            +---------------+
            | Calls webhook |
            | with order_id |
            +---------------+
                    |
                    v
            +---------------+
            | Your Server   |
            | Returns data  |
            +---------------+
                    |
                    v
            +---------------+
            | AI formulates |
            | response      |
            +---------------+
                    |
                    v
Customer: "Your order #12345 shipped via FedEx
           and will arrive January 15th."
```

## Best Practices

  - **Write Clear Descriptions**: The AI uses the description to decide when to use the tool. Be specific about when it should and shouldn't be used.
  - **Use HTTPS**: Webhook URLs must use HTTPS for security. We reject HTTP endpoints.
  - **Handle Errors Gracefully**: Return meaningful error messages so the agent can inform the user appropriately.
  - **Respond Quickly**: Keep webhook response times under 10 seconds. The user is waiting for a response.

### Example Tool Descriptions

**Good description:**
```text
Look up the status and tracking information for a customer order.
Use when the customer asks about their order status, shipping progress,
delivery estimate, or tracking number. Requires the order ID which
typically starts with "ORD-" followed by numbers.
```

**Poor description:**
```text
Get order info.
```

> **Warning:**
> Poor descriptions lead to tools being used incorrectly or not at all. Invest time in writing clear, detailed descriptions.

## Security Considerations

1. **Always use HTTPS** for webhook endpoints
2. **Verify signatures** using the webhook secret
3. **Validate parameters** before processing
4. **Rate limit** your webhook endpoints
5. **Log requests** for debugging and auditing
6. **Never expose** sensitive data in tool responses that shouldn't be shared with customers

## Next Steps

  - [Add Knowledge Base](https://www.zavu.dev/docs/guides/ai-agents/knowledge-base): Let your agent answer questions from documents
  - [Create Flows](https://www.zavu.dev/docs/guides/ai-agents/flows): Build structured conversation paths that use tools
