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

# Webhooks

> Receive real-time HTTP notifications when tools execute, workflows finish, or spend limits are hit

Danube webhooks send HTTPS POST requests to your server when events happen on your account. Use them to trigger downstream actions, log activity, or alert your team — without polling.

## Quick Start

<Steps>
  <Step title="Create a webhook">
    Go to [Dashboard > Webhooks](https://danubeai.com/dashboard/webhooks), click **Add Webhook**, enter your HTTPS endpoint URL, and select the events you want to receive.

    You can also create one via the [API](/api-reference/endpoint/create_webhook):

    ```bash theme={null}
    curl -X POST https://api.danubeai.com/v1/webhooks \
      -H "Authorization: Bearer YOUR_JWT" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/webhooks/danube",
        "events": ["tool.execution.completed", "tool.execution.failed"]
      }'
    ```
  </Step>

  <Step title="Save your signing secret">
    After creation you'll see a signing secret (starts with `whsec_`). **Copy it immediately** — it won't be shown again. You'll use this to verify that incoming requests actually came from Danube.
  </Step>

  <Step title="Build your endpoint">
    Your server needs to do three things:

    1. Read the raw request body and parse the signature header
    2. Verify the HMAC signature (which includes a timestamp for replay protection)
    3. Return a `2xx` status code within 10 seconds

    Here's a minimal example:

    <CodeGroup>
      ```python Python (Flask) theme={null}
      import hashlib, hmac, time
      from flask import Flask, request, abort

      app = Flask(__name__)
      WEBHOOK_SECRET = "whsec_your_secret_here"
      MAX_AGE_SECONDS = 300  # Reject deliveries older than 5 minutes

      def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool:
          """Verify a Danube webhook signature with replay protection.

          Signature format: t={unix_timestamp},sha256={hex_digest}
          Signed content:   {timestamp}.{payload}
          """
          parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p)
          ts = parts.get("t", "")
          sig = parts.get("sha256", "")

          if not ts or not sig:
              return False

          # Reject old deliveries to prevent replay attacks
          if abs(time.time() - int(ts)) > MAX_AGE_SECONDS:
              return False

          signed_content = f"{ts}.{payload.decode()}"
          expected = hmac.new(
              secret.encode(), signed_content.encode(), hashlib.sha256
          ).hexdigest()

          return hmac.compare_digest(expected, sig)

      @app.route("/webhooks/danube", methods=["POST"])
      def handle_webhook():
          payload = request.data
          signature = request.headers.get("X-Danube-Signature", "")

          if not verify_webhook(payload, signature, WEBHOOK_SECRET):
              abort(401)

          event = request.json
          print(f"{event['event']}: {event['data']}")
          return "", 200
      ```

      ```javascript Node.js (Express) theme={null}
      const crypto = require('crypto');
      const express = require('express');
      const app = express();

      const WEBHOOK_SECRET = 'whsec_your_secret_here';
      const MAX_AGE_SECONDS = 300; // Reject deliveries older than 5 minutes

      function verifyWebhook(payload, signatureHeader, secret) {
        // Signature format: t={unix_timestamp},sha256={hex_digest}
        const parts = Object.fromEntries(
          signatureHeader.split(',').map(p => p.split('=', 2))
        );
        const ts = parts.t;
        const sig = parts.sha256;

        if (!ts || !sig) return false;

        // Reject old deliveries to prevent replay attacks
        if (Math.abs(Date.now() / 1000 - parseInt(ts)) > MAX_AGE_SECONDS) {
          return false;
        }

        const signedContent = `${ts}.${payload}`;
        const expected = crypto
          .createHmac('sha256', secret)
          .update(signedContent, 'utf-8')
          .digest('hex');

        return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
      }

      app.post('/webhooks/danube',
        express.raw({ type: 'application/json' }),
        (req, res) => {
          const payload = req.body.toString();
          const signature = req.headers['x-danube-signature'] || '';

          if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {
            return res.status(401).send('Invalid signature');
          }

          const event = JSON.parse(payload);
          console.log(`${event.event}:`, event.data);
          res.sendStatus(200);
        }
      );
      ```

      ```go Go theme={null}
      package main

      import (
          "crypto/hmac"
          "crypto/sha256"
          "encoding/hex"
          "encoding/json"
          "fmt"
          "io"
          "math"
          "net/http"
          "strconv"
          "strings"
          "time"
      )

      const webhookSecret = "whsec_your_secret_here"
      const maxAgeSeconds = 300

      func verifyWebhook(body []byte, signatureHeader, secret string) bool {
          // Parse "t={ts},sha256={hex}"
          parts := make(map[string]string)
          for _, segment := range strings.Split(signatureHeader, ",") {
              kv := strings.SplitN(segment, "=", 2)
              if len(kv) == 2 {
                  parts[kv[0]] = kv[1]
              }
          }

          ts, hasTsm := parts["t"]
          sig, hasSig := parts["sha256"]
          if !hasTsm || !hasSig {
              return false
          }

          // Replay protection
          tsInt, err := strconv.ParseInt(ts, 10, 64)
          if err != nil || math.Abs(float64(time.Now().Unix()-tsInt)) > maxAgeSeconds {
              return false
          }

          signedContent := ts + "." + string(body)
          mac := hmac.New(sha256.New, []byte(secret))
          mac.Write([]byte(signedContent))
          expected := hex.EncodeToString(mac.Sum(nil))

          return hmac.Equal([]byte(expected), []byte(sig))
      }

      func handler(w http.ResponseWriter, r *http.Request) {
          body, _ := io.ReadAll(r.Body)
          signature := r.Header.Get("X-Danube-Signature")

          if !verifyWebhook(body, signature, webhookSecret) {
              http.Error(w, "Invalid signature", http.StatusUnauthorized)
              return
          }

          var event map[string]interface{}
          json.Unmarshal(body, &event)
          fmt.Printf("%s: %v\n", event["event"], event["data"])
          w.WriteHeader(http.StatusOK)
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Test it">
    Trigger an event (e.g. execute a tool) and check the delivery status in [Dashboard > Webhooks](https://danubeai.com/dashboard/webhooks). Expand your webhook to see recent deliveries, including HTTP status codes and response bodies.
  </Step>
</Steps>

## Event Types

| Event                           | Fires when                                    |
| ------------------------------- | --------------------------------------------- |
| `tool.execution.completed`      | A tool ran successfully                       |
| `tool.execution.failed`         | A tool execution errored                      |
| `workflow.completed`            | All workflow steps finished                   |
| `workflow.failed`               | Any workflow step errored                     |
| `agent.spend.limit_approaching` | Daily spend reached 80% of an API key's limit |

## Payload Format

Every delivery uses this envelope:

```json theme={null}
{
  "event": "tool.execution.completed",
  "timestamp": "2026-02-24T15:30:45.123456Z",
  "data": {
    // Event-specific fields
  }
}
```

<AccordionGroup>
  <Accordion title="tool.execution.completed / tool.execution.failed">
    ```json theme={null}
    {
      "event": "tool.execution.completed",
      "timestamp": "2026-02-24T15:30:45.123456Z",
      "data": {
        "tool_id": "abc-123",
        "tool_name": "Gmail - Send Email",
        "status": "success",
        "execution_time": 1.234,
        "error": null
      }
    }
    ```
  </Accordion>

  <Accordion title="workflow.completed / workflow.failed">
    ```json theme={null}
    {
      "event": "workflow.completed",
      "timestamp": "2026-02-24T15:30:45.123456Z",
      "data": {
        "workflow_id": "wf-456",
        "execution_id": "exec-789",
        "status": "completed",
        "execution_time_ms": 4567,
        "steps_completed": 3,
        "steps_total": 3,
        "error": null
      }
    }
    ```
  </Accordion>

  <Accordion title="agent.spend.limit_approaching">
    ```json theme={null}
    {
      "event": "agent.spend.limit_approaching",
      "timestamp": "2026-02-24T15:30:45.123456Z",
      "data": {
        "daily_limit_cents": 50000,
        "spent_today_cents": 40000,
        "percentage": 80.0
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Signature Verification

Every delivery includes a signed `X-Danube-Signature` header with a timestamp for replay protection. **Always verify it** before processing.

### How it works

The signature header has the format:

```
t=1740412245,sha256=5d2a...f8e1
```

1. Danube computes `HMAC-SHA256("{timestamp}.{raw_body}", your_secret)` and sends the result along with the timestamp
2. Your server reconstructs the same signed content (`{timestamp}.{raw_body}`) and computes the HMAC using the secret you saved at creation
3. Compare the two digests using a constant-time comparison to prevent timing attacks
4. **Reject deliveries where the timestamp is too old** (e.g. more than 5 minutes) to prevent replay attacks

### Request Headers

| Header               | Value                                                       |
| -------------------- | ----------------------------------------------------------- |
| `X-Danube-Signature` | `t={unix_ts},sha256={hex_digest}` — timestamped HMAC-SHA256 |
| `X-Danube-Event`     | Event type, e.g. `tool.execution.completed`                 |
| `X-Danube-Delivery`  | Unique delivery UUID (use to deduplicate)                   |
| `Content-Type`       | `application/json`                                          |
| `User-Agent`         | `DanubeAI-Webhooks/1.0`                                     |

## Retries and Failures

If your endpoint doesn't return a `2xx` or the request times out, Danube retries with exponential backoff:

| Attempt   | Delay before retry |
| --------- | ------------------ |
| 1st retry | \~1 second         |
| 2nd retry | \~4 seconds        |

After 3 failed attempts the delivery is marked as **failed**. You can inspect delivery history (status codes, response bodies, attempt counts) in the [dashboard](https://danubeai.com/dashboard/webhooks).

## Best Practices

<CardGroup cols={2}>
  <Card title="Respond fast" icon="bolt">
    Return `200` immediately, then process the event asynchronously. The delivery times out after 10 seconds.
  </Card>

  <Card title="Verify signatures" icon="shield-halved">
    Always validate `X-Danube-Signature` before trusting the payload. Never skip this in production.
  </Card>

  <Card title="Check the timestamp" icon="clock">
    Reject signatures where `t` is more than 5 minutes old to prevent replay attacks.
  </Card>

  <Card title="Handle duplicates" icon="copy">
    Use the `X-Danube-Delivery` UUID to deduplicate. Network retries may deliver the same event more than once.
  </Card>

  <Card title="Use HTTPS" icon="lock">
    Webhook URLs must use `https://`. Danube will not deliver to plain HTTP or internal/private endpoints.
  </Card>
</CardGroup>

## API Reference

<CardGroup cols={2}>
  <Card title="List Webhooks" icon="list" href="/api-reference/endpoint/list_webhooks">
    Get all your registered webhooks
  </Card>

  <Card title="Create Webhook" icon="plus" href="/api-reference/endpoint/create_webhook">
    Register a new webhook endpoint
  </Card>

  <Card title="Update Webhook" icon="pen" href="/api-reference/endpoint/update_webhook">
    Change URL, events, or active status
  </Card>

  <Card title="View Deliveries" icon="clock-rotate-left" href="/api-reference/endpoint/get_webhook_deliveries">
    Inspect delivery history and debug failures
  </Card>
</CardGroup>
