Skip to main content
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

1

Create a webhook

Go to 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:
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"]
  }'
2

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.
3

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:
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
4

Test it

Trigger an event (e.g. execute a tool) and check the delivery status in Dashboard > Webhooks. Expand your webhook to see recent deliveries, including HTTP status codes and response bodies.

Event Types

EventFires when
tool.execution.completedA tool ran successfully
tool.execution.failedA tool execution errored
workflow.completedAll workflow steps finished
workflow.failedAny workflow step errored
agent.spend.limit_approachingDaily spend reached 80% of an API key’s limit

Payload Format

Every delivery uses this envelope:
{
  "event": "tool.execution.completed",
  "timestamp": "2026-02-24T15:30:45.123456Z",
  "data": {
    // Event-specific fields
  }
}
{
  "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
  }
}
{
  "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
  }
}
{
  "event": "agent.spend.limit_approaching",
  "timestamp": "2026-02-24T15:30:45.123456Z",
  "data": {
    "daily_limit_cents": 50000,
    "spent_today_cents": 40000,
    "percentage": 80.0
  }
}

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

HeaderValue
X-Danube-Signaturet={unix_ts},sha256={hex_digest} — timestamped HMAC-SHA256
X-Danube-EventEvent type, e.g. tool.execution.completed
X-Danube-DeliveryUnique delivery UUID (use to deduplicate)
Content-Typeapplication/json
User-AgentDanubeAI-Webhooks/1.0

Retries and Failures

If your endpoint doesn’t return a 2xx or the request times out, Danube retries with exponential backoff:
AttemptDelay 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.

Best Practices

Respond fast

Return 200 immediately, then process the event asynchronously. The delivery times out after 10 seconds.

Verify signatures

Always validate X-Danube-Signature before trusting the payload. Never skip this in production.

Check the timestamp

Reject signatures where t is more than 5 minutes old to prevent replay attacks.

Handle duplicates

Use the X-Danube-Delivery UUID to deduplicate. Network retries may deliver the same event more than once.

Use HTTPS

Webhook URLs must use https://. Danube will not deliver to plain HTTP or internal/private endpoints.

API Reference