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

# Webhook Setup

> Configure webhooks to receive live call status and session completion notifications

# Webhook Setup

Webhooks notify your server when events occur in Vocobase, such as a call status changing or a session completing. Instead of polling the API, you receive a POST request with the event payload.

## What webhooks are for

Vocobase sends two classes of webhook events to each enabled webhook endpoint:

| Event                 | Purpose                                                                 | Use it for                                                                                              |
| --------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `call.status.updated` | Real-time phone call lifecycle updates from the telephony provider      | Partner queue orchestration, live UI state, freeing concurrency when the call reaches a terminal status |
| `session.completed`   | Final Vocobase session data after the bot and post-call pipeline finish | Transcript, recording URL, credits, variables, extraction, and final call metadata                      |

For outbound telephony calls, use `call.status.updated` to know what is happening right now. Use `session.completed` only when you need final post-call data.

`call.status.updated` includes:

* Current normalized call status
* Previous status, when available
* Final call disposition and answer classification when available
* Hangup cause/source when the provider supplied them
* Whether the status is terminal
* Whether the call slot/concurrency can be released
* Account-wide concurrency snapshot for queue orchestration
* Provider and raw provider status for debugging

`session.completed` includes:

* Full conversation transcript with timestamps
* Call duration and credits used
* Recording download URL (if recording was enabled)
* Pre-call variables and post-call extraction results
* Call metadata (phone numbers, provider, direction)

## Configure webhook endpoints

Use webhook endpoints to send the same event stream to one or more destinations, such as `prod`, `staging`, or an internal audit receiver. Each endpoint has its own URL, enabled flag, delivery history, and signing secret.

```bash theme={null}
curl -X POST https://api.vocobase.com/api/v2/config/webhooks \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "prod",
    "url": "https://your-server.com/webhooks/vocobase"
  }'
```

The response contains a `secret` that you must save:

```json theme={null}
{
  "success": true,
  "data": {
    "id": "12345678-abcd-1234-abcd-123456789012",
    "label": "prod",
    "url": "https://your-server.com/webhooks/vocobase",
    "enabled": true,
    "last_delivery_at": null,
    "last_delivery_status": null,
    "created_at": "2026-05-25T10:30:00.000Z",
    "updated_at": "2026-05-25T10:30:00.000Z",
    "secret": "whsec_a1b2c3d4e5f6g7h8i9j0...",
    "message": "Save this secret — it will not be shown again."
  }
}
```

<Warning>
  The endpoint secret is only shown once. Store it in your environment variables immediately. If you lose it, rotate the endpoint secret with `POST /config/webhooks/{id}/rotate-secret`.
</Warning>

### Manage endpoints

List configured endpoints:

```bash theme={null}
curl https://api.vocobase.com/api/v2/config/webhooks \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012"
```

Update a label, URL, or enabled state:

```bash theme={null}
curl -X PATCH https://api.vocobase.com/api/v2/config/webhooks/12345678-abcd-1234-abcd-123456789012 \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'
```

Rotate a signing secret:

```bash theme={null}
curl -X POST https://api.vocobase.com/api/v2/config/webhooks/12345678-abcd-1234-abcd-123456789012/rotate-secret \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012"
```

Delete an endpoint:

```bash theme={null}
curl -X DELETE https://api.vocobase.com/api/v2/config/webhooks/12345678-abcd-1234-abcd-123456789012 \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012"
```

### Labels and limits

* Labels must be lowercase letters, numbers, and hyphens; start with a letter or number; and be 1-31 characters.
* The `default` label is reserved for the legacy single-webhook compatibility route.
* Each partner can configure up to 5 webhook endpoints.
* Only enabled endpoints receive new events.
* Endpoints receive all webhook event types. Branch on the top-level `event` field in your handler.
* Each endpoint signs requests with its own secret.

<Note>
  Existing integrations that use `PUT /api/v2/config/webhook` still work. That route now creates or replaces the reserved `default` endpoint and returns a `webhook_secret`. New integrations should use `/config/webhooks`.
</Note>

## Requirements

Your webhook endpoint must:

* Accept `POST` requests
* Use HTTPS (HTTP URLs are rejected)
* Return a `2xx` status code within 10 seconds
* Be publicly accessible from the internet

## Signature verification

Every webhook request includes an `X-Webhook-Signature` header for verifying authenticity. The signature is computed as an HMAC-SHA256 hash of `timestamp.body` using your webhook secret.

### Header format

```
X-Webhook-Signature: sha256=<hex-encoded-hmac>
X-Webhook-Timestamp: <iso-8601-timestamp>
```

### Verification steps

1. Extract the `X-Webhook-Signature` and `X-Webhook-Timestamp` headers
2. Construct the signed payload: `{timestamp}.{raw_request_body}`
3. Compute HMAC-SHA256 using your webhook secret
4. Compare the computed signature with the one in the header

### Node.js example

```javascript theme={null}
const crypto = require("crypto");

// IMPORTANT: Use express.raw() or express.text() middleware on your webhook
// route so req.body is the raw string, not a parsed JSON object.
// Example: app.post("/webhooks/vocobase", express.text({ type: "*/*" }), handler)
function verifyWebhookSignature(req, webhookSecret) {
  const signature = req.headers["x-webhook-signature"];
  const timestamp = req.headers["x-webhook-timestamp"];
  // req.body must be the raw request body string, not a parsed object
  const body = typeof req.body === "string" ? req.body : req.rawBody?.toString() || JSON.stringify(req.body);

  // Reject requests older than 5 minutes to prevent replay attacks
  const timestampMs = new Date(timestamp).getTime();
  if (Math.abs(Date.now() - timestampMs) > 300_000) {
    throw new Error("Webhook timestamp too old");
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${body}`;
  const expectedSignature =
    "sha256=" +
    crypto
      .createHmac("sha256", webhookSecret)
      .update(signedPayload)
      .digest("hex");

  // Timing-safe comparison
  if (
    !crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    )
  ) {
    throw new Error("Invalid webhook signature");
  }

  return true;
}

// Express.js handler
app.post("/webhooks/vocobase", (req, res) => {
  try {
    verifyWebhookSignature(req, process.env.VOCOBASE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(401).json({ error: err.message });
  }

  const event = req.body;
  console.log("Received event:", event.event, event.data?.session_id);

  // Process the webhook asynchronously
  processWebhookAsync(event);

  // Return 200 immediately
  res.status(200).json({ received: true });
});
```

### Python example

```python theme={null}
import hmac
import hashlib
import os
from datetime import datetime, timezone
from flask import Flask, request, jsonify

app = Flask(__name__)

def verify_webhook_signature(request, webhook_secret):
    signature = request.headers.get("X-Webhook-Signature")
    timestamp = request.headers.get("X-Webhook-Timestamp")
    body = request.get_data(as_text=True)

    # Reject requests older than 5 minutes
    ts = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
    age = abs((datetime.now(timezone.utc) - ts).total_seconds())
    if age > 300:
        raise ValueError("Webhook timestamp too old")

    # Compute expected signature
    signed_payload = f"{timestamp}.{body}"
    expected = "sha256=" + hmac.new(
        webhook_secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid webhook signature")

@app.post("/webhooks/vocobase")
def handle_webhook():
    try:
        verify_webhook_signature(request, os.environ["VOCOBASE_WEBHOOK_SECRET"])
    except ValueError as e:
        return jsonify({"error": str(e)}), 401

    event = request.json
    # Process asynchronously, return immediately
    return jsonify({"received": True}), 200
```

<Tip>
  Always verify signatures in production. Skipping verification exposes your endpoint to forged requests.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Webhook Payloads" icon="file-code" href="/webhooks/payloads">
    See the full payload structure for each event type.
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/webhooks/error-handling">
    Learn about retry behavior and best practices.
  </Card>
</CardGroup>
