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

# Webhooks API — Real-Time Call Event Notifications

> Register webhook endpoints to receive real-time notifications from VInfer when calls complete, dispositions are set, or escalations occur.

Instead of polling the Calls API repeatedly to check for updates, you can register a webhook — an HTTPS endpoint on your server — and VInfer will push a `POST` request to it the moment a relevant event occurs. Webhooks make your integration event-driven: your system reacts to call completions, dispositions, and escalations in real time without any unnecessary API traffic.

***

## Available Events

| Event                  | Description                                                                  |
| ---------------------- | ---------------------------------------------------------------------------- |
| `call.completed`       | A call has finished (either connected and ended, or was not answered).       |
| `call.disposition_set` | A disposition label was assigned to a completed call.                        |
| `call.escalated`       | A call was escalated to a human agent during the conversation.               |
| `campaign.started`     | A scheduled campaign has begun dialing.                                      |
| `campaign.paused`      | A running campaign was paused (via API or dashboard).                        |
| `campaign.completed`   | All contacts in a campaign have been attempted and the campaign is finished. |

***

## Register a Webhook

<code>POST /webhooks</code>

Registers a new webhook endpoint to receive events for your workspace.

### Request Body

<ParamField body="url" type="string" required>
  The HTTPS URL on your server that will receive event payloads. Must be publicly reachable by VInfer's servers. HTTP (non-TLS) URLs are not accepted.
</ParamField>

<ParamField body="events" type="array" required>
  Array of event type strings to subscribe to. You can subscribe to one or all available events. Example: `["call.completed", "call.escalated"]`. Pass `["*"]` to subscribe to all events.
</ParamField>

<ParamField body="secret" type="string">
  Optional — a secret string used to sign webhook payloads with HMAC-SHA256. If provided, VInfer includes an `X-VInfer-Signature` header on every delivery so you can verify the payload originated from VInfer. See [Signature Verification](#signature-verification).
</ParamField>

### Example Request

```bash theme={null}
curl https://api.vinfer.ai/v1/webhooks \
  -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.example.com/webhooks/vinfer",
    "events": ["call.completed", "call.escalated", "campaign.completed"],
    "secret": "whsec_your_random_secret_string"
  }'
```

### Example Response

```json theme={null}
{
  "id": "wh_5Tq2nX8mB",
  "url": "https://your-server.example.com/webhooks/vinfer",
  "events": ["call.completed", "call.escalated", "campaign.completed"],
  "status": "active",
  "created_at": "2024-01-28T16:45:00Z"
}
```

***

## List Webhooks

<code>GET /webhooks</code>

Returns all registered webhooks for your workspace, including their subscription status and last delivery result.

### Example Request

```bash theme={null}
curl https://api.vinfer.ai/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY"
```

***

## Delete a Webhook

<code>DELETE /webhooks/\{id}</code>

Permanently removes a registered webhook. VInfer will stop sending events to the associated URL immediately. This action cannot be undone — if you need to re-enable the webhook, register it again via `POST /webhooks`.

### Example Request

```bash theme={null}
curl https://api.vinfer.ai/v1/webhooks/wh_5Tq2nX8mB \
  -X DELETE \
  -H "Authorization: Bearer YOUR_API_KEY"
```

***

## Event Payload Structure

All events share a common JSON envelope. The `event` field identifies the event type, `id` is a unique identifier for this delivery (useful for deduplication), `timestamp` is when the event was generated, and `data` contains the event-specific payload.

```json theme={null}
{
  "event": "call.completed",
  "id": "evt_abc123xyz",
  "timestamp": "2024-01-15T10:35:22Z",
  "data": { }
}
```

### `call.completed` Event

<Accordion title="call.completed payload example">
  ```json theme={null}
  {
    "event": "call.completed",
    "id": "evt_8Kp3mL7qN",
    "timestamp": "2024-02-01T09:16:25Z",
    "data": {
      "call_id": "cal_9Hm3kP7qZ",
      "campaign_id": "cmp_4Rv8sT1wX",
      "contact": {
        "phone": "+919876543210",
        "name": "Priya Sharma"
      },
      "status": "completed",
      "disposition": "escalated",
      "duration_seconds": 142,
      "language": "hi-IN",
      "started_at": "2024-02-01T09:14:03Z",
      "ended_at": "2024-02-01T09:16:25Z",
      "transcript_job_id": "job_2Kn7wR4pM",
      "recording_url": "https://recordings.vinfer.ai/signed/cal_9Hm3kP7qZ?token=eyJ...&expires=1706783785"
    }
  }
  ```
</Accordion>

### `call.escalated` Event

<Accordion title="call.escalated payload example">
  ```json theme={null}
  {
    "event": "call.escalated",
    "id": "evt_3Wm9sR2pK",
    "timestamp": "2024-02-01T09:15:50Z",
    "data": {
      "call_id": "cal_9Hm3kP7qZ",
      "campaign_id": "cmp_4Rv8sT1wX",
      "contact": {
        "phone": "+919876543210",
        "name": "Priya Sharma"
      },
      "escalation_reason": "customer_requested_agent",
      "escalated_at": "2024-02-01T09:15:50Z",
      "language": "hi-IN",
      "summary": "Customer expressed interest and requested to speak with a human agent about loan renewal options."
    }
  }
  ```
</Accordion>

<Note>
  Use the `id` field in the event envelope to implement deduplication in your handler. In rare cases (network retries, failover), your endpoint may receive the same event more than once — storing processed event IDs lets you safely skip duplicates.
</Note>

***

## Signature Verification

When you register a webhook with a `secret`, VInfer signs every payload using **HMAC-SHA256** and includes the signature in the `X-VInfer-Signature` header as `sha256=<hex_digest>`. Verifying this signature confirms the payload genuinely came from VInfer and has not been tampered with.

To verify:

1. Read the raw request body as bytes (do not parse JSON first).
2. Compute the HMAC-SHA256 of the raw body using your secret as the key.
3. Compare your computed digest to the value in `X-VInfer-Signature` (after stripping the `sha256=` prefix).
4. Use a constant-time comparison to prevent timing attacks.

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_vinfer_signature(payload_bytes: bytes, header: str, secret: str) -> bool:
      expected = "sha256=" + hmac.new(
          secret.encode("utf-8"),
          payload_bytes,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, header)

  # In your Flask/FastAPI handler:
  # payload_bytes = request.get_data()          # raw body, before JSON parsing
  # header = request.headers.get("X-VInfer-Signature")
  # if not verify_vinfer_signature(payload_bytes, header, "whsec_your_secret"):
  #     return 401
  ```

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

  function verifyVInferSignature(payloadBuffer, header, secret) {
    if (!header || !header.startsWith('sha256=')) return false;
    const expectedSig = 'sha256=' + crypto
      .createHmac('sha256', secret)
      .update(payloadBuffer)
      .digest('hex');
    const expectedBuf = Buffer.from(expectedSig);
    const headerBuf = Buffer.from(header);
    // Buffers must be the same length for timingSafeEqual; length mismatch itself
    // is not secret, so an early false return here is safe.
    if (expectedBuf.length !== headerBuf.length) return false;
    // Use timingSafeEqual to prevent timing attacks
    return crypto.timingSafeEqual(expectedBuf, headerBuf);
  }

  // In your Express handler:
  // app.post('/webhooks/vinfer', express.raw({ type: 'application/json' }), (req, res) => {
  //   const header = req.headers['x-vinfer-signature'];
  //   if (!verifyVInferSignature(req.body, header, 'whsec_your_secret')) {
  //     return res.status(401).send('Invalid signature');
  //   }
  //   const event = JSON.parse(req.body);
  //   // handle event...
  //   res.status(200).send('OK');
  // });
  ```
</CodeGroup>

<Warning>
  Always verify the signature in production. Without verification, any party who discovers your webhook URL can send forged events to your endpoint. Never use string equality (`===`) for signature comparison — use a constant-time function to prevent timing side-channel attacks.
</Warning>

***

## Retry Behavior

If your endpoint does not respond with a `2xx` status code within **10 seconds**, VInfer treats the delivery as failed and retries with exponential backoff:

| Attempt   | Delay after previous attempt |
| --------- | ---------------------------- |
| 1st retry | 1 minute                     |
| 2nd retry | 5 minutes                    |
| 3rd retry | 30 minutes                   |
| 4th retry | 2 hours                      |
| 5th retry | 8 hours                      |

After 5 failed retries, VInfer stops attempting delivery for that event. The failed delivery is logged in the dashboard under **Settings → Webhooks → Delivery Log** so you can investigate and replay events manually if needed.

<Tip>
  Respond with `200 OK` as quickly as possible — ideally before doing any heavy processing. Push the event payload to an internal queue (e.g., Redis, SQS, or a database task table) and process it asynchronously. This keeps your endpoint fast and prevents timeouts from triggering unnecessary retries.
</Tip>
