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

# Webhooks

> Receive real-time notifications when events occur

## Overview

Webhooks allow you to receive HTTP POST notifications when events occur in your Voicy account. When a call ends, Voicy sends the full call data — transcript, summary, extracted variables, and more — to your server in real time.

Each agent supports up to **5 webhooks**, all firing in parallel. Use them to push call data to your CRM, analytics platform, or any custom integration.

## Creating a Webhook

<Steps>
  <Step title="Open Agent Settings">
    In the [Voicy Dashboard](https://app.voicy.co), navigate to your agent and open the **Settings** tab. Scroll down to the **Webhooks** section and expand it.
  </Step>

  <Step title="Add a Webhook">
    Click the **+** button to add a new webhook. Fill in the fields:

    | Field              | Required | Description                                                                   |
    | ------------------ | -------- | ----------------------------------------------------------------------------- |
    | **Name**           | Yes      | A label to identify this webhook (e.g. "CRM", "Analytics", "Slack")           |
    | **URL**            | Yes      | The HTTPS endpoint that will receive POST requests                            |
    | **Signing Secret** | No       | A secret string for HMAC-SHA256 signature verification                        |
    | **Custom Headers** | No       | Key-value pairs added to every request (e.g. `Authorization: Bearer <token>`) |

    Click **Add** to save.
  </Step>

  <Step title="Deploy the Agent">
    Webhooks are part of the agent configuration. After adding or modifying webhooks, **deploy the agent** for changes to take effect on phone calls.

    <Note>
      Changes take effect immediately for web (test) calls, but phone calls use the deployed version.
    </Note>
  </Step>

  <Step title="Test It">
    Make a test call from the dashboard's built-in call simulator. When the call ends, your endpoint will receive a POST request with the full call payload.

    <Tip>
      Use a service like [webhook.site](https://webhook.site) to inspect payloads during development.
    </Tip>
  </Step>
</Steps>

### Managing Webhooks

* **Edit** — Click the pencil icon on any webhook to change its name, URL, or secret. Leave the secret field blank to keep the existing secret.
* **Delete** — Click the trash icon to remove a webhook.
* **Limit** — Up to 5 webhooks per agent. The badge shows your current count (e.g. "2/5").

### Custom Headers

Custom headers let you attach arbitrary HTTP headers to every webhook request — useful when your receiving endpoint requires authentication (e.g. `Authorization: Bearer <token>`) or a vendor-specific API key header.

**Limits:** Up to 10 headers per webhook. Names must be valid [RFC 7230 tokens](https://httpwg.org/specs/rfc7230.html#rule.token.separators). Values cannot contain line breaks.

**Reserved headers** that cannot be overridden: `Content-Type`, `x-voicy-event`, `x-voicy-signature`, `Host`, `Content-Length`, `Transfer-Encoding`.

**Security:** Header values are stored server-side and never returned by the API (same treatment as signing secrets). When editing a webhook, leave all value fields blank to preserve the existing headers.

## Events

### `call_ended`

Fired after all post-call processing completes (summary generation, variable extraction, email notifications). The payload contains the full call data in the same format as the [Get Call](/api-reference/call-get) API.

**Headers:**

| Header              | Description                                                                   |
| ------------------- | ----------------------------------------------------------------------------- |
| Custom headers      | Any headers configured for this webhook, sent before the Voicy system headers |
| `Content-Type`      | `application/json`                                                            |
| `x-voicy-event`     | Event name (`call_ended`)                                                     |
| `x-voicy-signature` | HMAC-SHA256 signature (only if signing secret is configured)                  |

**Payload:**

```json theme={null}
{
  "event": "call_ended",
  "call": {
    "call_id": "550e8400-e29b-41d4-a716-446655440000",
    "call_type": "phone_call",
    "call_status": "user_hangup",
    "agent": {
      "id": "agent-uuid",
      "name": "My Agent",
      "version": 3
    },
    "from_number": "+972501234567",
    "to_number": "+972521234567",
    "direction": "incoming",
    "start_timestamp": 1710000000000,
    "duration_ms": 45000,
    "transcript": [
      { "role": "agent", "content": "שלום, איך אוכל לעזור?" },
      { "role": "user", "content": "אני צריך עזרה" }
    ],
    "summary": "הלקוח ביקש עזרה בנושא...",
    "vars_provided": { "from_number": "+972501234567" },
    "vars_extracted": { "customer_name": "משה" },
    "vars_collected": { "email": "user@example.com" },
    "call_cost": { "combined_cost": 0.0234, "currency": "usd" }
  }
}
```

## Handling Webhooks

When your server receives a webhook, respond with a `200` status code to acknowledge receipt. Here's a minimal handler:

<CodeGroup>
  ```javascript Express.js theme={null}
  const express = require('express');
  const app = express();

  app.post('/voicy-webhook', express.json(), (req, res) => {
    const { event, call } = req.body;

    console.log(`Event: ${event}`);
    console.log(`Call ID: ${call.call_id}`);
    console.log(`Status: ${call.call_status}`);
    console.log(`Duration: ${call.duration_ms}ms`);

    if (call.summary) {
      console.log(`Summary: ${call.summary}`);
    }

    if (call.vars_extracted) {
      console.log('Extracted vars:', call.vars_extracted);
    }

    res.status(200).send('OK');
  });

  app.listen(3000);
  ```

  ```python Flask theme={null}
  from flask import Flask, request

  app = Flask(__name__)

  @app.route('/voicy-webhook', methods=['POST'])
  def webhook():
      data = request.json
      event = data['event']
      call = data['call']

      print(f"Event: {event}")
      print(f"Call ID: {call['call_id']}")
      print(f"Status: {call['call_status']}")
      print(f"Duration: {call['duration_ms']}ms")

      if call.get('summary'):
          print(f"Summary: {call['summary']}")

      if call.get('vars_extracted'):
          print(f"Extracted vars: {call['vars_extracted']}")

      return 'OK', 200
  ```
</CodeGroup>

## Signature Verification

If a signing secret is configured, each webhook request includes an `x-voicy-signature` header containing an HMAC-SHA256 signature of the raw request body. Always verify signatures in production to ensure requests are genuinely from Voicy.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhook(body, signature, secret) {
    const expected = 'sha256=' + crypto
      .createHmac('sha256', secret)
      .update(body)
      .digest('hex');
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  }

  // Express middleware
  app.post('/voicy-webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-voicy-signature'];
    if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }
    const event = JSON.parse(req.body);
    console.log('Call ended:', event.call.call_id);
    res.status(200).send('OK');
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
      expected = 'sha256=' + hmac.new(
          secret.encode(), body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)

  # Flask example
  from flask import Flask, request, abort

  app = Flask(__name__)

  @app.route('/voicy-webhook', methods=['POST'])
  def webhook():
      signature = request.headers.get('x-voicy-signature', '')
      if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
          abort(401)

      data = request.json
      print(f"Call ended: {data['call']['call_id']}")
      return 'OK', 200
  ```
</CodeGroup>

## Behavior

| Property         | Value                                  |
| ---------------- | -------------------------------------- |
| **Method**       | POST                                   |
| **Timeout**      | 5 seconds                              |
| **Retries**      | None (fire-and-forget)                 |
| **Content-Type** | `application/json`                     |
| **Parallel**     | All agent webhooks fire simultaneously |

If a webhook endpoint is unreachable or returns an error, the failure is logged but does not affect call processing or other webhooks.
