# Webhooks

Get events pushed to your HTTPS endpoint instead of polling. Needs `webhooks:manage`. Subscribing
an endpoint to an event also needs that record's read scope: `lead.created` needs `leads:read`,
`call.completed` needs `calls:read`, `booking.created` needs `bookings:read`,
`outbound_call.completed` needs `outbound:write`, every `chat.*` event needs `chats:read`, and
`chat.lead_captured` also needs `leads:read`.

Each delivery is a `POST` with a JSON body `{"id", "type", "created_at", "data"}` and these headers:
`Vocenya-Event` (the event type), `Vocenya-Delivery` (the event id; use it to ignore duplicates) and
`Vocenya-Signature: t=<unix time>,v1=<hex>`. To verify, compute HMAC-SHA256 of `"<t>.<raw body>"`
with your endpoint's secret, compare it to `v1` in constant time, and reject timestamps more than
5 minutes old.

Answer with any 2xx within 10 seconds. Anything else is retried with backoff (1 min, 5 min, 15 min,
1 h, 3 h, 6 h, 12 h) for up to 8 attempts. Payloads carry ids, statuses, times and contact
details, never conversation content, except `chat.message.created`, which carries the message so
your system can answer it. HIPAA-mode accounts get ids only. Fetch the full record from the API
when you need more.

Events: `lead.created`, `call.completed`, `booking.created`, `outbound_call.completed`, and for
website live chat `chat.started`, `chat.message.created`, `chat.lead_captured`,
`chat.handoff_requested`, `chat.handed_off` and `chat.closed`.

Live chat events share a `chat` object: `{"id", "status", "page_url", "handed_off_at", "closed_at",
"created_at"}`. `chat.message.created` adds `message`: `{"id", "sender", "role", "author", "body",
"via_api", "created_at"}`, where `sender` is `visitor`, `ai`, `agent`, `owner` or `system` and `role`
is what the widget shows (`visitor`, `ai`, `team` or `system`); skip `via_api: true`, your own
replies. `chat.lead_captured` adds `lead` (the `lead.created` fields). `chat.handoff_requested`
adds `requested_by` (`visitor` or `ai`) when a person is asked for; `chat.handed_off` also fires
when your team takes a chat over by replying. `chat.closed` adds `chat.lead_id` and
`chat.message_count`. Example `chat.message.created`:

```json
{"id": "9f0c...", "type": "chat.message.created", "created_at": "2026-10-02T14:03:30+00:00",
 "data": {"chat": {"id": 205, "status": "ai", "page_url": "https://www.example.com/contact",
   "handed_off_at": null, "closed_at": null, "created_at": "2026-10-02T14:03:11+00:00"},
  "message": {"id": 4410, "sender": "visitor", "role": "visitor", "author": "Visitor",
   "body": "Hi, do you have any openings this week?", "via_api": false,
   "created_at": "2026-10-02T14:03:30+00:00"}}}
```

The same event for a HIPAA-mode account: `{"chat": {"id": 205}, "message": {"id": 4410}}`.

Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.

- [List webhook endpoints](#list-webhook-endpoints): `GET /webhook-endpoints`
- [Create a webhook endpoint](#create-webhook-endpoint): `POST /webhook-endpoints`
- [Delete a webhook endpoint](#delete-webhook-endpoint): `DELETE /webhook-endpoints/{webhookEndpoint}`
- [Send a test event](#test-webhook-endpoint): `POST /webhook-endpoints/{webhookEndpoint}/test`

## List webhook endpoints

`GET https://vocenya.com/api/public/v1/webhook-endpoints`

Required scope: `webhooks:manage`

### Responses

- `200`: Array of `WebhookEndpointResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `webhooks:manage` scope.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.

### Response attributes (200)

- `data` (array of objects, required)
  - `id` (integer, required)
  - `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
  - `url` (string, required)
  - `description` (string or null, required)
  - `events` (array of strings, required): The events sent to this endpoint. One of: `lead.created`, `call.completed`, `booking.created`, `chat.handed_off`, `outbound_call.completed`, `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.closed`.
  - `enabled` (boolean, required)
  - `created_at` (string or null, required)

### Example request

```bash
curl https://vocenya.com/api/public/v1/webhook-endpoints \
  -H "Authorization: Bearer $VOCENYA_API_KEY"
```

```javascript
const response = await fetch('https://vocenya.com/api/public/v1/webhook-endpoints', {
  headers: {
    Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
  },
});

const data = await response.json();
console.log(data);
```

```php
$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://vocenya.com/api/public/v1/webhook-endpoints', [
    'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
]);

$data = json_decode((string) $response->getBody(), true);
print_r($data);
```

```python
import os

import requests

response = requests.get(
    "https://vocenya.com/api/public/v1/webhook-endpoints",
    headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)

print(response.json())
```

### Example response (200)

```json
{
  "data": [
    {
      "id": 1,
      "livemode": true,
      "url": "https://hooks.example.com/vocenya",
      "description": "string",
      "events": [
        "lead.created"
      ],
      "enabled": true,
      "created_at": "2026-10-02T14:03:11+00:00"
    }
  ]
}
```


## Create a webhook endpoint

`POST https://vocenya.com/api/public/v1/webhook-endpoints`

Required scope: `webhooks:manage`

Add an endpoint. The response includes its signing `secret`: store it now, it is not shown again.

### Request body

- `url` (string (uri), required): A public HTTPS URL. Up to 2048 characters.
- `events` (array of strings, required): The events to send, at least one. The key needs the read scope of each event's record: `lead.created` needs `leads:read`, `call.completed` needs `calls:read`, `booking.created` needs `bookings:read`, `outbound_call.completed` needs `outbound:write`, every `chat.*` event needs `chats:read`, and `chat.lead_captured` also needs `leads:read`. One of: `lead.created`, `call.completed`, `booking.created`, `chat.handed_off`, `outbound_call.completed`, `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.closed`. At least 1 items.
- `description` (string or null, optional): Up to 255 characters.

### Responses

- `201`: The endpoint and its signing secret.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key lacks the read scope of a subscribed event, for example `leads:read` for `lead.created` (`missing_scope`).
- `422`: Validation failed, the URL is not a public HTTPS address (`unsafe_url`), or the account already has 10 endpoints (`too_many_endpoints`).
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.

### Response attributes (201)

- `data` (object, required)
  - `id` (integer, required)
  - `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
  - `url` (string, required)
  - `description` (string or null, required)
  - `events` (array of strings, required): The events sent to this endpoint. One of: `lead.created`, `call.completed`, `booking.created`, `chat.handed_off`, `outbound_call.completed`, `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.closed`.
  - `enabled` (boolean, required)
  - `created_at` (string or null, required)
- `secret` (string, required)

### Example request

```bash
curl -X POST https://vocenya.com/api/public/v1/webhook-endpoints \
  -H "Authorization: Bearer $VOCENYA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://hooks.example.com/vocenya",
  "events": [
    "lead.created"
  ],
  "description": "Our CRM"
}'
```

```javascript
const response = await fetch('https://vocenya.com/api/public/v1/webhook-endpoints', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://hooks.example.com/vocenya",
    "events": [
      "lead.created"
    ],
    "description": "Our CRM"
  }),
});

const data = await response.json();
console.log(data);
```

```php
$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://vocenya.com/api/public/v1/webhook-endpoints', [
    'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
    'json' => [
        'url' => 'https://hooks.example.com/vocenya',
        'events' => [
            'lead.created',
        ],
        'description' => 'Our CRM',
    ],
]);

$data = json_decode((string) $response->getBody(), true);
print_r($data);
```

```python
import os

import requests

response = requests.post(
    "https://vocenya.com/api/public/v1/webhook-endpoints",
    headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
    json={
        "url": "https://hooks.example.com/vocenya",
        "events": [
            "lead.created",
        ],
        "description": "Our CRM",
    },
)

print(response.json())
```

### Example response (201)

```json
{
  "data": {
    "id": 3,
    "url": "https://hooks.example.com/vocenya",
    "description": "Our CRM",
    "events": [
      "lead.created"
    ],
    "enabled": true,
    "created_at": "2026-09-27T15:04:05+00:00"
  },
  "secret": "whsec_4hW0cB1m9GqS7pXvL2eYt8Kz3nRd6FjA5uVo0iQs"
}
```


## Delete a webhook endpoint

`DELETE https://vocenya.com/api/public/v1/webhook-endpoints/{webhookEndpoint}`

Required scope: `webhooks:manage`

Delete an endpoint. Queued deliveries to it are dropped.

### Path parameters

- `webhookEndpoint` (integer, required): The endpoint id.

### Responses

- `204`: Deleted.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `webhooks:manage` scope.
- `404`: No record with that id belongs to your account.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.

### Example request

```bash
curl -X DELETE https://vocenya.com/api/public/v1/webhook-endpoints/3 \
  -H "Authorization: Bearer $VOCENYA_API_KEY"
```

```javascript
const response = await fetch('https://vocenya.com/api/public/v1/webhook-endpoints/3', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
  },
});

console.log(response.status);
```

```php
$client = new \GuzzleHttp\Client();

$response = $client->request('DELETE', 'https://vocenya.com/api/public/v1/webhook-endpoints/3', [
    'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
]);

echo $response->getStatusCode();
```

```python
import os

import requests

response = requests.delete(
    "https://vocenya.com/api/public/v1/webhook-endpoints/3",
    headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)

print(response.status_code)
```


## Send a test event

`POST https://vocenya.com/api/public/v1/webhook-endpoints/{webhookEndpoint}/test`

Required scope: `webhooks:manage`

Send a signed `webhook.test` event to this endpoint, with `{"message": "..."}` as its data. Pass
`event` to get a sample of a real event instead (for example `chat.message.created`), with made-up
data and `"sample": true`, cut down to ids for HIPAA-mode accounts like the real thing. It is
delivered in the background like any other event, even when the endpoint is paused.

```bash
curl -X POST https://vocenya.com/api/public/v1/webhook-endpoints/3/test \
  -H "Authorization: Bearer $VOCENYA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event": "chat.message.created"}'
```

### Path parameters

- `webhookEndpoint` (integer, required): The endpoint id.

### Request body

- `event` (string or null, optional): Send a sample of this event (made-up data plus `"sample": true`) instead of `webhook.test`. One of: `lead.created`, `call.completed`, `booking.created`, `chat.handed_off`, `outbound_call.completed`, `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.closed`.

### Responses

- `202`: The queued delivery.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `webhooks:manage` scope.
- `404`: No record with that id belongs to your account.
- `422`: Validation error
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.

### Response attributes (202)

- `data` (object, required)
  - `id` (integer, required)
  - `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
  - `event_id` (string, required): The event id, also sent as the `Vocenya-Delivery` header. Use it to ignore duplicates.
  - `event` (string, required)
  - `webhook_endpoint_id` (integer, required)
  - `status` (string, required): | | |---| | `pending` <br/> Waiting for its first or next attempt. | | `delivered` <br/> The endpoint answered with a 2xx status. | | `failed` <br/> Every attempt failed, or the endpoint was unsafe to call. | One of: `pending`, `delivered`, `failed`.
  - `attempts` (integer, required)
  - `response_status` (integer or null, required)
  - `last_error` (string or null, required)
  - `next_attempt_at` (string or null, required)
  - `delivered_at` (string or null, required)
  - `created_at` (string or null, required)

### Example request

```bash
curl -X POST https://vocenya.com/api/public/v1/webhook-endpoints/3/test \
  -H "Authorization: Bearer $VOCENYA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "event": "chat.message.created"
}'
```

```javascript
const response = await fetch('https://vocenya.com/api/public/v1/webhook-endpoints/3/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "event": "chat.message.created"
  }),
});

const data = await response.json();
console.log(data);
```

```php
$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://vocenya.com/api/public/v1/webhook-endpoints/3/test', [
    'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
    'json' => [
        'event' => 'chat.message.created',
    ],
]);

$data = json_decode((string) $response->getBody(), true);
print_r($data);
```

```python
import os

import requests

response = requests.post(
    "https://vocenya.com/api/public/v1/webhook-endpoints/3/test",
    headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
    json={
        "event": "chat.message.created",
    },
)

print(response.json())
```

### Example response (202)

```json
{
  "data": {
    "id": 1,
    "livemode": true,
    "event_id": "string",
    "event": "lead.created",
    "webhook_endpoint_id": 1,
    "status": "pending",
    "attempts": 1,
    "response_status": 1,
    "last_error": "string",
    "next_attempt_at": "2026-10-02T14:03:11+00:00",
    "delivered_at": "2026-10-02T14:03:11+00:00",
    "created_at": "2026-10-02T14:03:11+00:00"
  }
}
```

