# Vocenya Docs
Developer documentation for the Vocenya API: guides, the full REST reference, signed webhooks, the live chat widget and MCP servers for AI assistants.
- Base URL: `https://vocenya.com/api/public/v1`
- OpenAPI document: https://vocenya.com/docs/api.json
- Create an API key: https://vocenya.com/app/developers
- Docs MCP server (public, read-only): https://vocenya.com/mcp/docs
## Guides
- [Quickstart](https://vocenya.com/docs/quickstart.md)
- [Authentication and scopes](https://vocenya.com/docs/authentication.md)
- [Test mode](https://vocenya.com/docs/test-mode.md)
- [Pagination](https://vocenya.com/docs/pagination.md)
- [Errors](https://vocenya.com/docs/errors.md)
- [Rate limits](https://vocenya.com/docs/rate-limits.md)
- [Outbound calls and compliance](https://vocenya.com/docs/outbound-calls.md)
- [Changelog](https://vocenya.com/docs/changelog.md)
## API reference
- [Overview](https://vocenya.com/docs/reference.md)
- [Account](https://vocenya.com/docs/reference/account.md)
- [Calls](https://vocenya.com/docs/reference/calls.md)
- [Leads](https://vocenya.com/docs/reference/leads.md)
- [Bookings](https://vocenya.com/docs/reference/bookings.md)
- [Outbound calls](https://vocenya.com/docs/reference/outbound-calls.md)
- [Do Not Call](https://vocenya.com/docs/reference/do-not-call.md)
- [Blocked callers](https://vocenya.com/docs/reference/blocked-callers.md)
- [Chats](https://vocenya.com/docs/reference/chats.md)
- [Webhooks](https://vocenya.com/docs/reference/webhooks.md)
## Webhooks
- [Webhooks](https://vocenya.com/docs/webhooks.md)
- [Webhook events](https://vocenya.com/docs/webhook-events.md)
## Live chat
- [Live chat widget](https://vocenya.com/docs/live-chat.md)
## MCP
- [MCP servers](https://vocenya.com/docs/mcp.md)
---
# Quickstart
Create an API key and make your first request to the Vocenya API in a few minutes.
The Vocenya API gives your own systems the calls, leads, bookings and chats your AI receptionist handles, and lets them add leads, queue compliant outbound AI calls, keep your Do Not Call list and subscribe to webhooks. Everything is JSON over HTTPS.
## 1. Create an API key
An account owner creates keys in the Vocenya portal, under [Developers](https://vocenya.com/app/developers). Give the key a name you will recognise later (for example "CRM sync") and pick only the scopes the integration needs.
Start with a **test key**: switch the Developers page to **Test** before you create it. Test keys start with `vk_test_`, work on a separate set of sample data and never ring anyone, and they are available even before your account is fully set up. Switch to a live key (`vk_live_`) when your integration is ready. See [Test mode](https://vocenya.com/docs/test-mode).
The full key is shown **once**, when you create it. Vocenya stores only a hash, so copy it straight into your secrets manager. If you lose it, revoke it and create another.
Keep the key out of your code and read it from an environment variable. Every example in these docs uses `VOCENYA_API_KEY`:
```bash
export VOCENYA_API_KEY="paste-your-key-here"
```
## 2. Make your first request
`GET /me` works with any valid key and tells you which account and key you are using. It is the quickest way to check your setup.
```bash
curl https://vocenya.com/api/public/v1/me \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/me', {
headers: { Authorization: `Bearer ${process.env.VOCENYA_API_KEY}` },
});
console.log(await response.json());
```
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://vocenya.com/api/public/v1/me', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
]);
print_r(json_decode((string) $response->getBody(), true));
```
```python
import os
import requests
response = requests.get(
"https://vocenya.com/api/public/v1/me",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
The response names your organization, whether it is in HIPAA mode, whether the key is live or test (`data.livemode`), and the key's own name, prefix and scopes. A `401` means the key is missing, mistyped, expired or revoked.
## 3. Read your leads
With the `leads:read` scope, list the most recent leads:
```bash
curl "https://vocenya.com/api/public/v1/leads?per_page=5" \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
Lists are newest first and come back as `{"data": [...], "links": {...}, "meta": {...}}`. Pass `meta.next_cursor` back as `cursor` for the next page: see [Pagination](https://vocenya.com/docs/pagination).
## 4. Create a lead
With `leads:write`, add a lead from another system, such as a website form. `reason` is the only required field.
```bash
curl -X POST https://vocenya.com/api/public/v1/leads \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Maria Rivera", "phone_number": "(512) 555-0123", "reason": "Wants a quote for a water heater replacement"}'
```
The response is `201 Created` with the new lead in `data`.
## Next steps
- [Authentication and scopes](https://vocenya.com/docs/authentication): what each scope allows.
- [Webhooks](https://vocenya.com/docs/webhooks): get leads, calls and chats pushed to you instead of polling.
- [Outbound calls](https://vocenya.com/docs/outbound-calls): have the AI call people back, within the calling rules.
- [API reference](https://vocenya.com/docs/reference): every endpoint, with code samples.
- Working with an AI assistant? Connect it to the [MCP servers](https://vocenya.com/docs/mcp).
---
# Authentication and scopes
Authenticate every request with an organization API key sent as a Bearer token, and give each key only the scopes it needs.
Every request to the Vocenya API, and to the platform MCP server, is authenticated with an **organization API key**. There is no user session: the key is the credential. (AI apps can also connect to the platform MCP server with [OAuth](https://vocenya.com/docs/mcp#connect-with-oauth-no-key), where an owner approves the same scopes on a consent screen.)
## Sending the key
Send the key in the `Authorization` header as a Bearer token:
```bash
curl https://vocenya.com/api/public/v1/me -H "Authorization: Bearer $VOCENYA_API_KEY"
```
The key is only read from the `Authorization` header, never from the query string or the request body.
## Live and test keys
| Prefix | Mode | Works on |
| --- | --- | --- |
| `vk_live_` | Live | Your real calls, leads, bookings and chats. Outbound calls ring real people. |
| `vk_test_` | Test | A separate set of test data. Test calls never ring anyone. |
Both kinds are followed by 40 random characters and use the same endpoints, scopes and limits. A key only sees records of its own mode: an id from the other mode returns `404`, and every object carries `livemode`. Test keys are available on every account, even before setup is finished; live keys once the account is set up. See [Test mode](https://vocenya.com/docs/test-mode).
## Who owns a key
Keys belong to your Vocenya **organization**, not to the person who created them, so an integration keeps working when a teammate leaves. Account owners create and revoke keys in the portal under [Developers](https://vocenya.com/app/developers).
- The full key is shown once, at creation. Vocenya keeps only a SHA-256 hash and a short prefix (like `vk_live_a1b2c3d4` or `vk_test_a1b2c3d4`) so you can tell keys apart.
- A key can be revoked at any time; it stops working immediately.
- A key can also carry an expiry date, after which it is refused.
- Each key records when it was last used, shown on the Developers page.
## Scopes
Each key carries a list of scopes. A request to an endpoint whose scope the key lacks is refused with `403` and the code `missing_scope`; the [API reference](https://vocenya.com/docs/reference) shows the scope every endpoint needs.
| Scope | Allows |
| --- | --- |
| `calls:read` | Read calls |
| `leads:read` | Read leads |
| `leads:write` | Create leads |
| `bookings:read` | Read bookings |
| `outbound:write` | Queue outbound AI calls and check their status |
| `dnc:read` | Read the Do Not Call list |
| `dnc:write` | Add to and remove from the Do Not Call list |
| `chats:read` | Read website chats |
| `chats:write` | Reply to and close website chats |
| `webhooks:manage` | Manage webhooks |
A few actions need two scopes:
- Creating a lead with a `callback` (an immediate AI call back) needs `leads:write` **and** `outbound:write`.
- Subscribing a webhook endpoint to an event needs `webhooks:manage` plus the read scope of that event's record, for example `leads:read` for `lead.created`. See [Webhook events](https://vocenya.com/docs/webhook-events).
The same scopes apply to the tools of the [platform MCP server](https://vocenya.com/docs/mcp).
## Errors
| Status | When |
| --- | --- |
| `401` `unauthenticated` | The key is missing, malformed, unknown, expired or revoked. The response carries a `WWW-Authenticate: Bearer` header. |
| `403` `missing_scope` | The key is valid but lacks the scope this endpoint needs. |
| `429` `too_many_failed_attempts` | More than 30 requests a minute with a missing or invalid key from one IP address. Wait for the `Retry-After` seconds. |
## HIPAA mode
For organizations in HIPAA mode, the API and the MCP server never return call transcripts, AI summaries, the reason and details of a lead, or chat messages, and webhooks carry ids only. Call recordings are never available through the API for any account. `GET /me` tells you whether the account is in HIPAA mode (`data.organization.hipaa_mode`).
## Keeping keys safe
- Store keys in environment variables or a secrets manager, never in source control or front-end code.
- Use one key per integration, with only the scopes it needs, so you can revoke one without breaking the others.
- Rotate a key by creating the new one, deploying it, then revoking the old one.
---
# Test mode
Build and test against a separate set of test data with vk_test_ keys. Test calls never ring anyone.
Every Vocenya account has a test mode alongside live mode. Test mode uses the same API, endpoints and MCP server, but works on a separate set of test data and never affects anyone in the real world.
## Test keys
- Live keys start with `vk_live_`. Test keys start with `vk_test_`.
- Account owners create and revoke both on the **Developers** page of the portal. Use the **Live / Test** toggle to switch. Each key has its own scopes, and a key is shown only once.
- Test keys are available on every account, including free trials, accounts that have not started billing, and signups that have not finished setup. Live keys are available once your account is set up.
- `GET /me` tells you which kind of key you are using: `data.livemode` is `false` and `data.api_key.mode` is `"test"` for a test key.
## Test data
- A test key only sees and changes test data. A live key only sees and changes live data. An id from the other mode returns `404`.
- Every object in a response has a `livemode` field: `true` for live data, `false` for test data.
- Test data never shows in the portal and never counts toward usage, billing, partner commissions, notifications, analytics or cost reports.
- When you create your first test key, sample data is added: four calls (two with transcripts), three leads, a booking, a website chat with messages, a completed outbound call, AI-call consent for `+15125550142` and `+15125550133`, and a Do Not Call entry for `+15125550199`.
- **Reset test data** on the Developers page (Test tab) deletes every test record and adds the sample data again. Live data, test keys and test webhook endpoints are kept.
## Outbound calls in test mode
- `POST /outbound-calls` (and `POST /leads` with a `callback`) checks a test call exactly like a live one: account settings (outbound calling on, terms accepted, active service, an AI receptionist, HIPAA rules), the test Do Not Call list plus Vocenya's own list, test consents, calling hours, the daily limit and calls already in progress. The only rule not checked is the free trial's AI-minute allowance.
- Refusals return `422` with the same `code` and `decision` as in live mode. A call outside calling hours is accepted with `decision.retry_at` and waits until then.
- An allowed test call never rings anyone. It moves from `queued` to `in_progress` to `completed` within a few seconds, with a sample transcript and summary (see `GET /outbound-calls/{outboundCall}`), and sends `outbound_call.completed` to your test webhook endpoints.
- To try it, queue a call to `+15125550142` (it has test consent). A call to `+15125550199` is refused with `do_not_call`.
- A call to `+15125550133` (also with test consent) reaches voicemail: it ends with status `voicemail` and `answered_by: "machine"`, and follows your account's voicemail setting like a live call (see [Voicemail](https://vocenya.com/docs/outbound-calls#voicemail)): under "leave a message" the message appears in the transcript with `voicemail_left_at` set; under "try again later" a retry is scheduled.
```bash
curl -X POST https://vocenya.com/api/public/v1/outbound-calls \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone_number": "+15125550142", "contact_name": "Test caller", "purpose": "informational"}'
```
## Chats in test mode
- Replies and closes with a test key work on test chats only and never reach a real visitor.
## Webhooks in test mode
- Webhook endpoints belong to one mode. Endpoints created with a test key, or on the Test tab of the Developers page, are test endpoints.
- Test endpoints only receive events from test data. Live endpoints only receive events from live data.
- Every delivery body includes `"livemode": true` or `"livemode": false`: `{"id", "type", "livemode", "created_at", "data"}`.
- **Send a test event** works for both live and test endpoints.
## MCP server
- The MCP server at `/mcp/platform` follows the key's mode in the same way: a test key's tools only see and change test data, and records include `livemode`.
---
# Pagination
Every list endpoint is newest first and cursor paginated. Pass meta.next_cursor back as cursor to walk through the pages.
List endpoints such as `GET /calls`, `GET /leads` and `GET /chats` return the newest records first and use **cursor pagination**: each page tells you where the next one starts, so records created while you page through never shift or repeat.
## Parameters
| Parameter | Description |
| --- | --- |
| `per_page` | How many records per page, from 1 to 100. Defaults to 25. |
| `cursor` | The `meta.next_cursor` (or `meta.prev_cursor`) of the page you just read. Leave it out for the first page. |
Most lists also take filters, such as `since` and `until` (ISO 8601 times) or a `status`. Keep the same filters on every page. The [API reference](https://vocenya.com/docs/reference) lists the filters of each endpoint.
## Response shape
```json
{
"data": [
{ "id": 311, "name": "Jordan Smith", "source": "chat", "created_at": "2026-10-02T14:05:40+00:00" }
],
"links": {
"first": null,
"last": null,
"prev": null,
"next": "https://vocenya.com/api/public/v1/leads?cursor=eyJpZCI6MzExLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9"
},
"meta": {
"path": "https://vocenya.com/api/public/v1/leads",
"per_page": 25,
"next_cursor": "eyJpZCI6MzExLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9",
"prev_cursor": null
}
}
```
`meta.next_cursor` is `null` on the last page. Treat cursors as opaque strings: pass them back exactly as you received them.
## Fetching every page
```javascript
async function fetchAllLeads() {
const leads = [];
let cursor = null;
do {
const url = new URL('https://vocenya.com/api/public/v1/leads');
url.searchParams.set('per_page', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.VOCENYA_API_KEY}` },
});
const page = await response.json();
leads.push(...page.data);
cursor = page.meta.next_cursor;
} while (cursor);
return leads;
}
```
```php
$client = new \GuzzleHttp\Client();
$leads = [];
$cursor = null;
do {
$response = $client->request('GET', 'https://vocenya.com/api/public/v1/leads', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
'query' => array_filter(['per_page' => 100, 'cursor' => $cursor]),
]);
$page = json_decode((string) $response->getBody(), true);
array_push($leads, ...$page['data']);
$cursor = $page['meta']['next_cursor'];
} while ($cursor !== null);
```
```python
import os
import requests
leads = []
cursor = None
while True:
params = {"per_page": 100}
if cursor:
params["cursor"] = cursor
page = requests.get(
"https://vocenya.com/api/public/v1/leads",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
params=params,
).json()
leads.extend(page["data"])
cursor = page["meta"]["next_cursor"]
if not cursor:
break
```
## Syncing new records
To keep another system up to date, prefer [webhooks](https://vocenya.com/docs/webhooks): they tell you about new leads, calls, bookings and chats as they happen. If you poll instead, pass `since` with the time of your last sync and page until `next_cursor` is `null`, staying within the [rate limit](https://vocenya.com/docs/rate-limits).
---
# Errors
The Vocenya API uses standard HTTP status codes and one JSON error shape, with a machine-readable code for every refusal you can act on.
Successful requests answer `200 OK`, `201 Created` (a record was created), `202 Accepted` (work was queued, like an outbound call or a test webhook) or `204 No Content` (a record was removed). Anything in the `4xx` range is a problem with the request; `5xx` means something went wrong on our side and is safe to retry later.
## Error shape
Every error is JSON with a human-readable `message` and a stable, machine-readable `code`. Branch on `code`, never on `message`, which may be reworded.
```json
{
"message": "This API key does not have the leads:write scope.",
"code": "missing_scope"
}
```
Validation errors (`422`, `validation_failed`) also list the problem with each field under `errors`:
```json
{
"message": "The reason field is required.",
"code": "validation_failed",
"errors": {
"reason": ["The reason field is required."]
}
}
```
A refused outbound call also carries the calling `decision`, explained in [Outbound calls](https://vocenya.com/docs/outbound-calls).
## Status codes
| Status | Meaning |
| --- | --- |
| `401` | The API key is missing, invalid, expired or revoked (`unauthenticated`). |
| `403` | The key lacks a scope (`missing_scope`), or the account or record does not allow the action. |
| `404` | No record with that id belongs to your account, or the path does not exist (`not_found`). Records of other accounts are never visible. |
| `405` | The path does not accept this method (`method_not_allowed`). |
| `409` | The record is in a state that does not allow the action, like replying to a closed chat (`chat_closed`). |
| `422` | Validation failed, or the action was refused with a `code`. |
| `429` | Too many requests (`rate_limited`). Wait for the number of seconds in the `Retry-After` header. See [Rate limits](https://vocenya.com/docs/rate-limits). |
| `5xx` | An error on our side (`server_error`). Retry with backoff. |
## Error codes
| `code` | Status | Meaning |
| --- | --- | --- |
| `validation_failed` | 422 | A field is missing or invalid; see `errors`. |
| `unauthenticated` | 401 | The API key is missing, invalid, expired or revoked. |
| `missing_scope` | 403 | The key does not have the scope this request needs. |
| `not_found` | 404 | That record, or that path, was not found. Records of the other mode (live or test) count as not found. |
| `method_not_allowed` | 405 | The path exists but not with this HTTP method. |
| `rate_limited` | 429 | Too many requests for this key. Wait for `Retry-After` seconds. |
| `too_many_failed_attempts` | 429 | Too many requests with a missing or invalid key from your IP address. |
| `server_error` | 500 | Something went wrong on our side. Retry later. |
| `live_chat_unavailable` | 403 | Website live chat is not part of your plan. |
| `chat_closed` | 409 | The chat has ended; it cannot be replied to. |
| `opt_out_permanent` | 403 | A person who asked not to be called cannot be removed from the Do Not Call list. |
| `invalid_number` | 422 | The phone number is not a valid US number. |
| `speed_to_lead_unavailable` | 422 | A lead `callback` was requested but the account cannot place AI callbacks. |
| `too_many_endpoints` | 422 | The account already has 10 webhook endpoints. |
| `unsafe_url` | 422 | The webhook URL is not a public HTTPS address. |
Outbound call refusals have their own codes, listed in [Outbound calls](https://vocenya.com/docs/outbound-calls#refusals).
## Retrying safely
The API does not take an `Idempotency-Key` header, but the endpoints you are most likely to retry are safe to repeat:
- `POST /outbound-calls` for a number that already has a call from the API waiting returns that same call instead of queuing another.
- `POST /do-not-call` for a number already on your list answers `200` with the existing entry (a new entry is `201`).
- A lead `callback` reuses a callback still waiting for the same number.
- Webhook deliveries carry a unique event id (`Vocenya-Delivery`), so you can ignore a delivery you have already processed.
`POST /leads` without a callback always creates a new lead, so retry it only when you know the first request did not reach us (a connection error rather than a `5xx`).
---
# Rate limits
Each API key can make a set number of requests a minute, shared between the REST API and the platform MCP server. Past it, wait for Retry-After.
Limits protect your account and the AI receptionist from a runaway integration. They are counted per API key, per minute.
## Limits
| What | Limit |
| --- | --- |
| Requests per API key | 120 a minute, shared by the REST API and the [platform MCP server](https://vocenya.com/docs/mcp) |
| Live chat replies (`POST /chats/{chat}/messages`) | 30 a minute per key, within the overall limit |
| Requests with a missing or invalid key | 30 a minute per IP address |
| Public MCP servers (`/mcp/site`, `/mcp/docs`) | 60 requests a minute per IP address each |
Need more for a real use case? Contact us and tell us what you are building.
## Headers
Responses carry the state of the per-key limit:
| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `Retry-After` | On a `429`, seconds to wait before trying again. |
## Handling 429
Over the limit, the API answers `429 Too Many Requests`. Wait for `Retry-After` seconds, then retry. A simple, polite client:
```javascript
async function vocenya(path, options = {}) {
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(`https://vocenya.com/api/public/v1${path}`, {
...options,
headers: {
Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
...options.headers,
},
});
if (response.status !== 429) {
return response;
}
const seconds = Number(response.headers.get('Retry-After') ?? 1);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
throw new Error('Still rate limited after 5 attempts');
}
```
```python
import os
import time
import requests
def vocenya(method, path, **kwargs):
headers = {"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"}
for _ in range(5):
response = requests.request(method, "https://vocenya.com/api/public/v1" + path, headers=headers, **kwargs)
if response.status_code != 429:
return response
time.sleep(int(response.headers.get("Retry-After", "1")))
raise RuntimeError("Still rate limited after 5 attempts")
```
## Staying under the limit
- Use [webhooks](https://vocenya.com/docs/webhooks) instead of polling for new records.
- Ask for 100 records per page when you page through a list.
- Give each integration its own key, so one busy job does not slow the others down.
---
# Outbound calls and compliance
Queue AI calls through the API. Every call is checked against consent, Do Not Call lists, calling hours and daily limits before it can ring.
`POST /outbound-calls` asks the AI receptionist to call someone, and `POST /leads` with a `callback` asks it to call a new lead back straight away (speed-to-lead). Calls from the API go through exactly the same checks as calls started in the portal, and the API cannot switch any of them off. A call is either **refused** (it will never be placed), **delayed** (accepted, it rings later) or **allowed** (it rings now).
These rules are how Vocenya applies the TCPA and state calling rules to AI voice calls. They are not legal advice: you remain responsible for having consent for the calls you ask for.
## Before you start
The account needs, all set in the portal:
- Outbound AI calling switched on, and the outbound calling terms accepted.
- An active subscription and an AI receptionist to make the call.
- For a lead `callback`: an active Pro plan too. Callbacks are not available in HIPAA mode.
The API key needs the `outbound:write` scope. To try the rules without ringing anyone, use a test key: test calls are checked the same way and then simulated. See [Test mode](https://vocenya.com/docs/test-mode#outbound-calls-in-test-mode).
## Website lead form
For leads from the business's own website you do not need the API: the portal's [Outbound calls](https://vocenya.com/app/outbound) page has a ready-made lead form under **Add the lead form to your website**, once speed-to-lead is on. Each lead it takes records the visitor's consent with the business's consent wording and queues the callback.
- **Embed (recommended).** One line where the form should appear. It loads the hosted form in an iframe that sizes itself, shows the business's consent text, and is protected by Cloudflare Turnstile without adding the website's domain anywhere:
```html
```
Copy it from the portal, which fills in the form key. Where a site builder blocks scripts, use the iframe code shown next to it. The same form has a direct link and a QR code to share on a Google Business Profile, social media or flyers, and an optional thank-you page on the website to send visitors to after they submit.
- **HTML form (advanced).** A plain form to style like the website, posting to the business's intake URL. It is protected by a hidden spam trap and rate limits, plus Cloudflare Turnstile when the business adds its own Turnstile keys in the portal (the snippet then includes the site key, and every lead is checked with the business's secret).
- **API.** From a server or CRM, create a lead with a `callback`, as below.
## Consent
An AI call needs consent to AI calls on record for that number:
- `purpose: "marketing"` (the default) needs **marketing** consent to AI calls.
- `purpose: "informational"` is for reminders and follow-ups the person asked for. Any active AI-call consent covers it.
- HIPAA-mode accounts may only make `informational` calls.
A lead `callback` records the consent as it queues the call: send `consent_text`, the exact wording the person agreed to, plus `source_url`, `ip_address` and `user_agent` from the moment they agreed when you have them.
```bash
curl -X POST https://vocenya.com/api/public/v1/leads \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Maria Rivera",
"phone_number": "(512) 555-0123",
"reason": "Wants a quote for a water heater replacement",
"callback": {
"consent_text": "I agree to receive an automated AI phone call from Rivera Plumbing about my request.",
"source_url": "https://www.example.com/contact"
}
}'
```
## Do Not Call
Numbers on your own Do Not Call list or on GH's list are never called. Manage yours with the [Do Not Call endpoints](https://vocenya.com/docs/reference/do-not-call). When a person asks not to be called, add them with `opted_out: true`: it becomes a permanent opt-out, their AI-call consent is revoked, and the entry cannot be removed (`opt_out_permanent`).
## Calling hours and limits
- Calls ring only between **8am to 8pm** in the time zone of the number being called. That is stricter than the federal 8am to 9pm, because several states end at 8pm.
- A number is called at most **3 times in 24 hours**.
- A number that is already on a call for your account is never rung twice at once.
None of these refuse the call: it is accepted and rings as soon as it is allowed.
## The decision
`POST /outbound-calls` answers `202 Accepted` with the queued call in `data` and a `decision`:
```json
{
"data": { "id": 88, "source": "api", "purpose": "marketing", "status": "queued", "phone_number": "+15125550123" },
"decision": {
"allowed": false,
"reason": "outside_hours",
"message": "It is outside calling hours (8am to 8pm) where that number is.",
"retry_at": "2026-09-29T08:00:00-05:00"
}
}
```
`decision.allowed` is `true` when the call can ring now. Otherwise `decision.retry_at` says when it will:
| `decision.reason` | Meaning |
| --- | --- |
| `outside_hours` | It is outside calling hours (8am to 8pm) where that number is. |
| `daily_limit` | That number has already been called three times in the last 24 hours. |
| `call_in_progress` | That number is already on a call for this client. |
Asking again for a number that already has a call from the API waiting returns that same call, so retries never stack up calls.
## Refusals
A call that can never be placed is refused with `422`, a `code` and, for calling rules, the `decision`:
| `code` | Meaning |
| --- | --- |
| `invalid_number` | That is not a valid US phone number. |
| `do_not_call` | That number is on a Do Not Call list. |
| `no_consent` | There is no consent on record for AI calls to that number. |
| `service_inactive` | The subscription is not active. |
| `terms_not_accepted` | The outbound calling terms have not been accepted. |
| `hipaa_marketing` | HIPAA accounts can only make appointment reminder calls. |
| `trial_minutes_exhausted` | The AI minutes included in the free trial are used up. Start the plan to make more AI calls. |
| `outbound_disabled` | Outbound AI calling is not switched on for this account. |
| `no_receptionist` | This account has no AI receptionist to make the call. |
Never try to work around a refusal, for example by changing the `purpose` without the matching consent.
## Voicemail
Every AI call runs the carrier's answering machine detection. A call that voicemail answers ends with status `voicemail` and `answered_by: "machine"`; a call a person answered has `answered_by: "human"` (`unknown` when detection could not tell, `null` when there was no result). What happens next is the account's voicemail setting, chosen on the **Outbound calls** page of the portal:
- **Leave a short message** (default): once the greeting ends, the virtual assistant says who called, why in one neutral sentence (never an appointment's details), and the number to call back. `voicemail_left_at` is set and the message is the last turn of the transcript.
- **Hang up and try again later**: a new call is scheduled about 90 minutes later with `blocked_reason: "voicemail_retry"`, within calling hours and the daily limit. The first call still ends as `voicemail`.
- **Hang up**: nothing is left.
## Following up
- `GET /outbound-calls/{outboundCall}` shows a call's status, `answered_by`, and `blocked_reason` when it was blocked at dial time (the rules are checked again just before dialing).
- Subscribe to [`outbound_call.completed`](https://vocenya.com/docs/webhook-events#outbound-call-completed) to hear when calls end.
---
# Changelog
Every change to the Vocenya API, webhooks and MCP servers, newest first, with the date it shipped.
The API is versioned in its URL (`/api/public/v1`). Within a version we only make additive changes: new endpoints, new optional parameters, new response fields and new webhook events. Build your integration to ignore fields and event types it does not know.
The current version is **1.0.0**.
## 1.0.0
### 3 October 2026
- Outbound calls now detect answering machines. Each organization chooses what happens when voicemail answers (leave a short message, hang up and retry later, or hang up); outbound calls and the `outbound_call.completed` webhook carry `answered_by` (`human`, `machine` or `unknown`) and `voicemail_left_at`. Test mode: `+15125550133` reaches voicemail. See [Outbound calls](https://vocenya.com/docs/outbound-calls).
- Spam handling for inbound calls: calls carry `spam` and `caller_name`; `GET /calls` takes `spam` and `status=blocked`; the `call.completed` webhook carries `spam`. New blocked-caller endpoints (`GET`, `POST`, `DELETE /blocked-callers`) and the `block_caller` MCP tool.
- `GET /calls` and the platform MCP server now return `spam` on every call.
### 2 October 2026
- Added test mode: `vk_test_` keys that work on a separate set of test data, simulated outbound calls, per-mode webhook endpoints, and a `livemode` field on every object and webhook delivery. See [Test mode](https://vocenya.com/docs/test-mode).
- Every error now returns a `code`, including `validation_failed`, `unauthenticated`, `not_found`, `method_not_allowed`, `rate_limited` and `server_error`.
- `POST /do-not-call` answers `200` with the existing entry when the number is already on the list.
- Added replying to live chats (`POST /chats/{chat}/messages`, scope `chats:write`) and closing them (`POST /chats/{chat}/close`).
- Added the `chats:write` scope.
- Added live chat webhook events: `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested` and `chat.closed`, alongside `chat.handed_off`.
- Test deliveries can now send a sample of a real event: `POST /webhook-endpoints/{webhookEndpoint}/test` with `event`.
- New developer docs at [/docs](https://vocenya.com/docs), with Markdown versions of every page and a public [docs MCP server](https://vocenya.com/docs/mcp#docs-mcp-server).
### 29 September 2026
First release of the Vocenya API:
- Organization API keys with scopes, and a per-key rate limit.
- Read calls, leads, bookings and chats; create leads, with an optional AI callback.
- Queue outbound AI calls, checked against consent, Do Not Call lists and calling hours.
- Read and change the Do Not Call list.
- Signed webhooks for `lead.created`, `call.completed`, `booking.created`, `outbound_call.completed` and `chat.handed_off`.
- The platform MCP server at `/mcp/platform` and the public site MCP server at `/mcp/site`.
---
# API reference
The Vocenya REST API is organized around resources, uses JSON for requests and responses, and standard HTTP status codes. Every request is authenticated with an organization API key.
- Base URL: `https://vocenya.com/api/public/v1`
- Authentication: `Authorization: Bearer $VOCENYA_API_KEY` ([Authentication](https://vocenya.com/docs/authentication))
- Lists: newest first, cursor paginated, up to 100 per page ([Pagination](https://vocenya.com/docs/pagination))
- Rate limit: 120 requests per minute per key ([Rate limits](https://vocenya.com/docs/rate-limits))
- Errors: `{"message": "...", "code": "..."}` ([Errors](https://vocenya.com/docs/errors))
- OpenAPI document: [https://vocenya.com/docs/api.json](https://vocenya.com/docs/api.json)
This reference is generated from the API code itself, so it always matches what the API does.
## Resources
### [Account](https://vocenya.com/docs/reference/account.md)
- `GET /me`: Get the current key
### [Calls](https://vocenya.com/docs/reference/calls.md)
- `GET /calls`: List calls (`calls:read`)
- `GET /calls/{call}`: Get a call (`calls:read`)
### [Leads](https://vocenya.com/docs/reference/leads.md)
- `GET /leads`: List leads (`leads:read`)
- `POST /leads`: Create a lead (`leads:write`)
- `GET /leads/{lead}`: Get a lead (`leads:read`)
### [Bookings](https://vocenya.com/docs/reference/bookings.md)
- `GET /bookings`: List bookings (`bookings:read`)
- `GET /bookings/{booking}`: Get a booking (`bookings:read`)
### [Outbound calls](https://vocenya.com/docs/reference/outbound-calls.md)
- `GET /outbound-calls`: List outbound calls (`outbound:write`)
- `POST /outbound-calls`: Queue an outbound AI call (`outbound:write`)
- `GET /outbound-calls/{outboundCall}`: Get an outbound call (`outbound:write`)
### [Do Not Call](https://vocenya.com/docs/reference/do-not-call.md)
- `GET /do-not-call`: List Do Not Call entries (`dnc:read`)
- `POST /do-not-call`: Add a number to the Do Not Call list (`dnc:write`)
- `DELETE /do-not-call/{entry}`: Remove a number from the Do Not Call list (`dnc:write`)
### [Blocked callers](https://vocenya.com/docs/reference/blocked-callers.md)
- `GET /blocked-callers`: List blocked callers (`dnc:read`)
- `POST /blocked-callers`: Block a caller (`dnc:write`)
- `DELETE /blocked-callers/{entry}`: Unblock a caller (`dnc:write`)
### [Chats](https://vocenya.com/docs/reference/chats.md)
- `GET /chats`: List chats (`chats:read`)
- `GET /chats/{chat}`: Get a chat (`chats:read`)
- `POST /chats/{chat}/messages`: Reply in a chat (`chats:write`)
- `POST /chats/{chat}/close`: Close a chat (`chats:write`)
### [Webhooks](https://vocenya.com/docs/reference/webhooks.md)
- `GET /webhook-endpoints`: List webhook endpoints (`webhooks:manage`)
- `POST /webhook-endpoints`: Create a webhook endpoint (`webhooks:manage`)
- `DELETE /webhook-endpoints/{webhookEndpoint}`: Delete a webhook endpoint (`webhooks:manage`)
- `POST /webhook-endpoints/{webhookEndpoint}/test`: Send a test event (`webhooks:manage`)
---
# Account
The account and key behind the request.
Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.
- [Get the current key](#get-me): `GET /me`
## Get the current key
`GET https://vocenya.com/api/public/v1/me`
Any valid API key.
Check a key and see which account it belongs to, what it may do and whether it is a live or a
test key. Needs no scope.
### Responses
- `200`
- `401`: The API key is missing, invalid, expired or revoked.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `livemode` (boolean, required): `false` when the key is a test key (`vk_test_`): every request then reads and writes test data only.
- `organization` (object, required)
- `id` (integer, required)
- `name` (string, required)
- `hipaa_mode` (boolean, required): When true, transcripts, summaries, lead reasons and chat messages are never returned, and webhooks carry ids only.
- `timezone` (string, required)
- `api_key` (object, required)
- `id` (integer, required)
- `name` (string, required)
- `prefix` (string, required)
- `mode` (string, required): `live` or `test`. One of: `live`, `test`.
- `scopes` (array of strings, required)
- `expires_at` (string or null, required)
### Example request
```bash
curl https://vocenya.com/api/public/v1/me \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/me', {
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/me', [
'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/me",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"livemode": true,
"organization": {
"id": 1,
"name": "Rivera Plumbing",
"hipaa_mode": true,
"timezone": "America/Chicago"
},
"api_key": {
"id": 1,
"name": "Zapier",
"prefix": "vk_live_a1b2c3d4",
"mode": "live",
"scopes": [
"leads:read",
"leads:write"
],
"expires_at": "2026-10-02T14:03:11+00:00"
}
}
}
```
---
# Calls
Phone calls your AI receptionist, GH Live agents and team handled. Needs `calls:read`.
Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.
- [List calls](#list-calls): `GET /calls`
- [Get a call](#get-call): `GET /calls/{call}`
## List calls
`GET https://vocenya.com/api/public/v1/calls`
Required scope: `calls:read`
Your calls, newest first.
### Query parameters
- `direction` (string, optional): One of: `inbound`, `outbound`.
- `status` (string, optional): | | |---| | `ringing`
| | `in_progress`
| | `completed`
| | `missed`
| | `failed`
| | `blocked`
The caller's number is on the client's blocked callers list: turned away before anyone answered. | One of: `ringing`, `in_progress`, `completed`, `missed`, `failed`, `blocked`.
- `since` (string (date-time), optional): Calls that started at or after this time (ISO 8601).
- `until` (string (date-time), optional): Calls that started before this time (ISO 8601).
- `spam` (string, optional): `true` for only spam and blocked calls, `false` to leave them out. Without it, every call is listed. One of: `true`, `false`, `1`, `0`.
- `per_page` (integer, optional): How many items per page, 1 to 100. Minimum 1. Maximum 100.
- `cursor` (string or null, optional): The `meta.next_cursor` (or `meta.prev_cursor`) of the previous page. Up to 1024 characters.
### Responses
- `200`: Paginated set of `CallResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `calls:read` scope.
- `422`: Validation error
- `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).
- `direction` (string, required): Whether the call came in to, or went out from, your number. One of: `inbound`, `outbound`.
- `status` (string, required): | | |---| | `ringing`
| | `in_progress`
| | `completed`
| | `missed`
| | `failed`
| | `blocked`
The caller's number is on the client's blocked callers list: turned away before anyone answered. | One of: `ringing`, `in_progress`, `completed`, `missed`, `failed`, `blocked`.
- `from_number` (string, required)
- `caller_name` (string or null, required): The caller's name as the carrier reported it (CNAM), when known.
- `to_number` (string, required)
- `spam` (boolean, required): Whether the call was judged spam: by the receptionist, or because the number is on your blocked callers list (`status` is then `blocked`).
- `routed_to` (string or null, required): Where the call was routed: the AI receptionist, a GH Live agent, your phone or voicemail. One of: `ai_agent`, `human_queue`, `forward_number`, `voicemail`.
- `started_at` (string, required)
- `answered_at` (string or null, required)
- `ended_at` (string or null, required)
- `duration_seconds` (integer or null, required)
- `disposition` (string or null, required)
- `summary` (string or null, optional): The AI's summary of the call. Not included for HIPAA-mode accounts.
- `handoff_summary` (string or null, optional): What the AI told the person it handed the call to. Not included for HIPAA-mode accounts.
- `transcript` (array of objects or null, optional): The conversation, turn by turn. Only on `GET /calls/{id}`, and never for HIPAA-mode accounts.
- `role` (string, required)
- `text` (string, required)
- `lead_ids` (array of anys, optional): Leads the AI captured on this call. Only on `GET /calls/{id}`.
- `created_at` (string or null, required)
- `links` (object, required)
- `first` (string or null, required)
- `last` (string or null, required)
- `prev` (string or null, required)
- `next` (string or null, required)
- `meta` (object, required)
- `path` (string or null, required): Base path for paginator generated URLs.
- `per_page` (integer, required): Number of items shown per page. Minimum 0.
- `next_cursor` (string or null, required): The "cursor" that points to the next set of items.
- `prev_cursor` (string or null, required): The "cursor" that points to the previous set of items.
### Example request
```bash
curl https://vocenya.com/api/public/v1/calls \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/calls', {
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/calls', [
'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/calls",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": [
{
"id": 1,
"livemode": true,
"direction": "inbound",
"status": "ringing",
"from_number": "+15125550123",
"caller_name": "string",
"to_number": "+15125550100",
"spam": true,
"routed_to": "ai_agent",
"started_at": "2026-10-02T14:03:11+00:00",
"answered_at": "2026-10-02T14:03:11+00:00",
"ended_at": "2026-10-02T14:03:11+00:00",
"duration_seconds": 1,
"disposition": "string",
"summary": "string",
"handoff_summary": "string",
"transcript": [
{
"role": "string",
"text": "string"
}
],
"lead_ids": [],
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"path": "string",
"per_page": 1,
"next_cursor": "string",
"prev_cursor": "string"
}
}
```
## Get a call
`GET https://vocenya.com/api/public/v1/calls/{call}`
Required scope: `calls:read`
One call with its transcript and the ids of the leads captured on it. HIPAA-mode accounts get
the call's ids, numbers, statuses and times only: no summary, handoff summary or transcript.
### Path parameters
- `call` (integer, required): The call id.
### Responses
- `200`: `CallResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `calls:read` scope.
- `404`: No record with that id belongs to your account.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `direction` (string, required): Whether the call came in to, or went out from, your number. One of: `inbound`, `outbound`.
- `status` (string, required): | | |---| | `ringing`
| | `in_progress`
| | `completed`
| | `missed`
| | `failed`
| | `blocked`
The caller's number is on the client's blocked callers list: turned away before anyone answered. | One of: `ringing`, `in_progress`, `completed`, `missed`, `failed`, `blocked`.
- `from_number` (string, required)
- `caller_name` (string or null, required): The caller's name as the carrier reported it (CNAM), when known.
- `to_number` (string, required)
- `spam` (boolean, required): Whether the call was judged spam: by the receptionist, or because the number is on your blocked callers list (`status` is then `blocked`).
- `routed_to` (string or null, required): Where the call was routed: the AI receptionist, a GH Live agent, your phone or voicemail. One of: `ai_agent`, `human_queue`, `forward_number`, `voicemail`.
- `started_at` (string, required)
- `answered_at` (string or null, required)
- `ended_at` (string or null, required)
- `duration_seconds` (integer or null, required)
- `disposition` (string or null, required)
- `summary` (string or null, optional): The AI's summary of the call. Not included for HIPAA-mode accounts.
- `handoff_summary` (string or null, optional): What the AI told the person it handed the call to. Not included for HIPAA-mode accounts.
- `transcript` (array of objects or null, optional): The conversation, turn by turn. Only on `GET /calls/{id}`, and never for HIPAA-mode accounts.
- `role` (string, required)
- `text` (string, required)
- `lead_ids` (array of anys, optional): Leads the AI captured on this call. Only on `GET /calls/{id}`.
- `created_at` (string or null, required)
### Example request
```bash
curl https://vocenya.com/api/public/v1/calls/1042 \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/calls/1042', {
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/calls/1042', [
'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/calls/1042",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"id": 1,
"livemode": true,
"direction": "inbound",
"status": "ringing",
"from_number": "+15125550123",
"caller_name": "string",
"to_number": "+15125550100",
"spam": true,
"routed_to": "ai_agent",
"started_at": "2026-10-02T14:03:11+00:00",
"answered_at": "2026-10-02T14:03:11+00:00",
"ended_at": "2026-10-02T14:03:11+00:00",
"duration_seconds": 1,
"disposition": "string",
"summary": "string",
"handoff_summary": "string",
"transcript": [
{
"role": "string",
"text": "string"
}
],
"lead_ids": [],
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
---
# Leads
People who called, chatted or filled in a form. Reading needs `leads:read`, creating needs `leads:write` (plus `outbound:write` to request an AI callback).
Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.
- [List leads](#list-leads): `GET /leads`
- [Create a lead](#create-lead): `POST /leads`
- [Get a lead](#get-lead): `GET /leads/{lead}`
## List leads
`GET https://vocenya.com/api/public/v1/leads`
Required scope: `leads:read`
Your leads, newest first.
### Query parameters
- `phone_number` (string, optional): Leads with this phone number, in any common US format. Up to 32 characters.
- `email` (string (email), optional): Up to 255 characters.
- `since` (string (date-time), optional): Leads created at or after this time (ISO 8601).
- `until` (string (date-time), optional): Leads created before this time (ISO 8601).
- `per_page` (integer, optional): How many items per page, 1 to 100. Minimum 1. Maximum 100.
- `cursor` (string or null, optional): The `meta.next_cursor` (or `meta.prev_cursor`) of the previous page. Up to 1024 characters.
### Responses
- `200`: Paginated set of `LeadResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `leads:read` scope.
- `422`: Validation error
- `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).
- `name` (string or null, required)
- `phone_number` (string or null, required)
- `email` (string or null, required)
- `reason` (string, optional): Why they got in touch. Not included for HIPAA-mode accounts.
- `urgency` (string or null, required)
- `details` (object or null, optional): Extra fields captured with the lead. Not included for HIPAA-mode accounts.
- `custom_fields` (object, optional): The custom fields the AI collected, as `{key: {label, value}}`. Not included for HIPAA-mode accounts.
- `source` (string, required): Where the lead came from. One of: `api`, `call`, `chat`, `outbound_call`, `website`.
- `call_id` (integer or null, required)
- `chat_id` (integer or null, required)
- `outbound_call_id` (integer or null, required)
- `created_at` (string or null, required)
- `links` (object, required)
- `first` (string or null, required)
- `last` (string or null, required)
- `prev` (string or null, required)
- `next` (string or null, required)
- `meta` (object, required)
- `path` (string or null, required): Base path for paginator generated URLs.
- `per_page` (integer, required): Number of items shown per page. Minimum 0.
- `next_cursor` (string or null, required): The "cursor" that points to the next set of items.
- `prev_cursor` (string or null, required): The "cursor" that points to the previous set of items.
### Example request
```bash
curl https://vocenya.com/api/public/v1/leads \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/leads', {
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/leads', [
'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/leads",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": [
{
"id": 1,
"livemode": true,
"name": "Maria Rivera",
"phone_number": "+15125550123",
"email": "maria@example.com",
"reason": "string",
"urgency": "high",
"details": {},
"custom_fields": {},
"source": "api",
"call_id": 1,
"chat_id": 1,
"outbound_call_id": 1,
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"path": "string",
"per_page": 1,
"next_cursor": "string",
"prev_cursor": "string"
}
}
```
## Create a lead
`POST https://vocenya.com/api/public/v1/leads`
Required scope: `leads:write`
Add a lead from your own system, for example a form on another site. It shows up in the portal
and is pushed to your CRM like any other lead.
Send `callback` to have the AI call the person back straight away (speed-to-lead). The key then
also needs the `outbound:write` scope. It needs outbound calling switched on, an active Pro plan and an AI receptionist, and is not available
in HIPAA mode. The call still follows calling hours, Do Not Call lists and daily limits; the
queued call is returned as `callback`.
### Request body
- `name` (string or null, optional): Up to 255 characters.
- `phone_number` (string or null, optional): A US phone number in any common format. Needed for a callback. Up to 32 characters.
- `email` (string (email) or null, optional): Up to 255 characters.
- `reason` (string, required): Why they got in touch. Up to 2000 characters.
- `urgency` (string or null, optional): Up to 50 characters.
- `details` (array of strings or null, optional): Any extra fields you want to keep with the lead (up to 50 keys). Up to 50 items.
- `callback` (object, optional): Ask the AI to call the person back now (speed-to-lead). Only send this when the person agreed to an AI call: `consent_text` must be the exact wording they agreed to. Needs the `outbound:write` scope as well as `leads:write`.
- `consent_text` (string, optional): At least 20 characters. Up to 2000 characters.
- `source_url` (string (uri) or null, optional): The page where they agreed. Up to 2048 characters.
- `ip_address` (string or null, optional): Their IP address when they agreed.
- `user_agent` (string or null, optional): Their browser's user agent when they agreed. Up to 512 characters.
### Responses
- `201`: The lead, and the AI callback when one was requested.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: A `callback` was sent but the key does not have the `outbound:write` scope (`missing_scope`).
- `422`: Validation failed, or a callback was requested but cannot be made (`speed_to_lead_unavailable`, `invalid_number`).
- `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).
- `name` (string or null, required)
- `phone_number` (string or null, required)
- `email` (string or null, required)
- `reason` (string, optional): Why they got in touch. Not included for HIPAA-mode accounts.
- `urgency` (string or null, required)
- `details` (object or null, optional): Extra fields captured with the lead. Not included for HIPAA-mode accounts.
- `custom_fields` (object, optional): The custom fields the AI collected, as `{key: {label, value}}`. Not included for HIPAA-mode accounts.
- `source` (string, required): Where the lead came from. One of: `api`, `call`, `chat`, `outbound_call`, `website`.
- `call_id` (integer or null, required)
- `chat_id` (integer or null, required)
- `outbound_call_id` (integer or null, required)
- `created_at` (string or null, required)
- `callback` (object or null, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `source` (string, required): What started the call: your system (`api`), a speed-to-lead callback, a campaign or an agent. | | |---| | `speed_to_lead`
The AI calls back someone who just asked to be contacted. | | `agent_dial`
A GH Live agent dials a number by hand from the workspace. | | `campaign`
The AI works through a client's campaign contact list. | | `api`
The client's own system queued an AI call through the public API or platform MCP server. | One of: `speed_to_lead`, `agent_dial`, `campaign`, `api`.
- `purpose` (string, required): `marketing` needs marketing consent; `informational` covers reminders and follow-ups the person asked for. One of: `marketing`, `informational`.
- `status` (string, required): One of: `queued`, `scheduled`, `blocked`, `connecting_agent`, `dialing`, `in_progress`, `completed`, `no_answer`, `voicemail`, `failed`.
- `contact_name` (string or null, required)
- `phone_number` (string, required)
- `blocked_reason` (string or null, required): Why the call was blocked or delayed, as a code.
- `blocked_reason_message` (string or null, required): Why the call was blocked or delayed, in words.
- `scheduled_for` (string or null, required): When a delayed call is due to ring (calling hours, daily limit).
- `attempts` (integer, required)
- `answered_at` (string or null, required)
- `answered_by` (string or null, required): Who picked up, from answering machine detection: `human`, `machine` or `unknown`. `null` when there was no result (not answered, or an agent's call). One of: `human`, `machine`, `unknown`.
- `voicemail_left_at` (string or null, required): When the AI finished leaving a message on voicemail, under the account's "leave a message" setting.
- `ended_at` (string or null, required)
- `duration_seconds` (integer or null, required)
- `summary` (string or null, optional): The AI's summary of the call. Not included for HIPAA-mode accounts.
- `transcript` (array of objects or null, optional): The conversation, turn by turn. Only on `GET /outbound-calls/{id}`, and never for HIPAA-mode accounts.
- `role` (string, required)
- `text` (string, required)
- `created_at` (string or null, required)
### Example request
```bash
curl -X POST https://vocenya.com/api/public/v1/leads \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Maria Rivera",
"email": "maria@example.com",
"reason": "Wants a quote for a water heater replacement",
"urgency": "high",
"callback": {
"consent_text": "I agree to receive an automated AI phone call from Rivera Plumbing about my request."
}
}'
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/leads', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Maria Rivera",
"email": "maria@example.com",
"reason": "Wants a quote for a water heater replacement",
"urgency": "high",
"callback": {
"consent_text": "I agree to receive an automated AI phone call from Rivera Plumbing about my request."
}
}),
});
const data = await response.json();
console.log(data);
```
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://vocenya.com/api/public/v1/leads', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
'json' => [
'name' => 'Maria Rivera',
'email' => 'maria@example.com',
'reason' => 'Wants a quote for a water heater replacement',
'urgency' => 'high',
'callback' => [
'consent_text' => 'I agree to receive an automated AI phone call from Rivera Plumbing about my request.',
],
],
]);
$data = json_decode((string) $response->getBody(), true);
print_r($data);
```
```python
import os
import requests
response = requests.post(
"https://vocenya.com/api/public/v1/leads",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
json={
"name": "Maria Rivera",
"email": "maria@example.com",
"reason": "Wants a quote for a water heater replacement",
"urgency": "high",
"callback": {
"consent_text": "I agree to receive an automated AI phone call from Rivera Plumbing about my request.",
},
},
)
print(response.json())
```
### Example response (201)
```json
{
"data": {
"id": 1,
"livemode": true,
"name": "Maria Rivera",
"phone_number": "+15125550123",
"email": "maria@example.com",
"reason": "string",
"urgency": "high",
"details": {},
"custom_fields": {},
"source": "api",
"call_id": 1,
"chat_id": 1,
"outbound_call_id": 1,
"created_at": "2026-10-02T14:03:11+00:00"
},
"callback": {
"id": 1,
"livemode": true,
"source": "speed_to_lead",
"purpose": "marketing",
"status": "queued",
"contact_name": "Maria Rivera",
"phone_number": "+15125550123",
"blocked_reason": "string",
"blocked_reason_message": "string",
"scheduled_for": "string",
"attempts": 1,
"answered_at": "2026-10-02T14:03:11+00:00",
"answered_by": "human",
"voicemail_left_at": "2026-10-02T14:03:11+00:00",
"ended_at": "2026-10-02T14:03:11+00:00",
"duration_seconds": 1,
"summary": "string",
"transcript": [
{
"role": "string",
"text": "string"
}
],
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
## Get a lead
`GET https://vocenya.com/api/public/v1/leads/{lead}`
Required scope: `leads:read`
### Path parameters
- `lead` (integer, required): The lead id.
### Responses
- `200`: `LeadResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `leads:read` scope.
- `404`: No record with that id belongs to your account.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `name` (string or null, required)
- `phone_number` (string or null, required)
- `email` (string or null, required)
- `reason` (string, optional): Why they got in touch. Not included for HIPAA-mode accounts.
- `urgency` (string or null, required)
- `details` (object or null, optional): Extra fields captured with the lead. Not included for HIPAA-mode accounts.
- `custom_fields` (object, optional): The custom fields the AI collected, as `{key: {label, value}}`. Not included for HIPAA-mode accounts.
- `source` (string, required): Where the lead came from. One of: `api`, `call`, `chat`, `outbound_call`, `website`.
- `call_id` (integer or null, required)
- `chat_id` (integer or null, required)
- `outbound_call_id` (integer or null, required)
- `created_at` (string or null, required)
### Example request
```bash
curl https://vocenya.com/api/public/v1/leads/311 \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/leads/311', {
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/leads/311', [
'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/leads/311",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"id": 1,
"livemode": true,
"name": "Maria Rivera",
"phone_number": "+15125550123",
"email": "maria@example.com",
"reason": "string",
"urgency": "high",
"details": {},
"custom_fields": {},
"source": "api",
"call_id": 1,
"chat_id": 1,
"outbound_call_id": 1,
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
---
# Bookings
Appointments the AI booked into your calendar or field-service software. Needs `bookings:read`.
Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.
- [List bookings](#list-bookings): `GET /bookings`
- [Get a booking](#get-booking): `GET /bookings/{booking}`
## List bookings
`GET https://vocenya.com/api/public/v1/bookings`
Required scope: `bookings:read`
Appointments the AI booked, most recently booked first. Filter by appointment time.
### Query parameters
- `starts_after` (string (date-time), optional): Appointments starting at or after this time (ISO 8601).
- `starts_before` (string (date-time), optional): Appointments starting before this time (ISO 8601).
- `per_page` (integer, optional): How many items per page, 1 to 100. Minimum 1. Maximum 100.
- `cursor` (string or null, optional): The `meta.next_cursor` (or `meta.prev_cursor`) of the previous page. Up to 1024 characters.
### Responses
- `200`: Paginated set of `BookingResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `bookings:read` scope.
- `422`: Validation error
- `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).
- `provider` (string, required): The calendar or software the appointment was booked into. One of: `google_calendar`, `microsoft_calendar`, `jobber`, `housecall_pro`, `servicetitan`, `acuity`.
- `external_reference` (string or null, required): The appointment's id in that calendar or software.
- `starts_at` (string, required)
- `duration_minutes` (integer, required)
- `name` (string or null, required)
- `phone_number` (string or null, required)
- `call_id` (integer or null, required)
- `outbound_call_id` (integer or null, required)
- `created_at` (string or null, required)
- `links` (object, required)
- `first` (string or null, required)
- `last` (string or null, required)
- `prev` (string or null, required)
- `next` (string or null, required)
- `meta` (object, required)
- `path` (string or null, required): Base path for paginator generated URLs.
- `per_page` (integer, required): Number of items shown per page. Minimum 0.
- `next_cursor` (string or null, required): The "cursor" that points to the next set of items.
- `prev_cursor` (string or null, required): The "cursor" that points to the previous set of items.
### Example request
```bash
curl https://vocenya.com/api/public/v1/bookings \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/bookings', {
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/bookings', [
'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/bookings",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": [
{
"id": 1,
"livemode": true,
"provider": "google_calendar",
"external_reference": "string",
"starts_at": "2026-10-02T14:03:11+00:00",
"duration_minutes": 1,
"name": "Maria Rivera",
"phone_number": "+15125550123",
"call_id": 1,
"outbound_call_id": 1,
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"path": "string",
"per_page": 1,
"next_cursor": "string",
"prev_cursor": "string"
}
}
```
## Get a booking
`GET https://vocenya.com/api/public/v1/bookings/{booking}`
Required scope: `bookings:read`
### Path parameters
- `booking` (integer, required): The booking id.
### Responses
- `200`: `BookingResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `bookings:read` scope.
- `404`: No record with that id belongs to your account.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `provider` (string, required): The calendar or software the appointment was booked into. One of: `google_calendar`, `microsoft_calendar`, `jobber`, `housecall_pro`, `servicetitan`, `acuity`.
- `external_reference` (string or null, required): The appointment's id in that calendar or software.
- `starts_at` (string, required)
- `duration_minutes` (integer, required)
- `name` (string or null, required)
- `phone_number` (string or null, required)
- `call_id` (integer or null, required)
- `outbound_call_id` (integer or null, required)
- `created_at` (string or null, required)
### Example request
```bash
curl https://vocenya.com/api/public/v1/bookings/64 \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/bookings/64', {
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/bookings/64', [
'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/bookings/64",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"id": 1,
"livemode": true,
"provider": "google_calendar",
"external_reference": "string",
"starts_at": "2026-10-02T14:03:11+00:00",
"duration_minutes": 1,
"name": "Maria Rivera",
"phone_number": "+15125550123",
"call_id": 1,
"outbound_call_id": 1,
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
---
# Outbound calls
Have the AI call someone who agreed to AI calls. Needs `outbound:write`.
Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.
- [List outbound calls](#list-outbound-calls): `GET /outbound-calls`
- [Queue an outbound AI call](#create-outbound-call): `POST /outbound-calls`
- [Get an outbound call](#get-outbound-call): `GET /outbound-calls/{outboundCall}`
## List outbound calls
`GET https://vocenya.com/api/public/v1/outbound-calls`
Required scope: `outbound:write`
Your outbound AI calls, newest first: the ones you queued and those from speed-to-lead,
campaigns and agents.
### Query parameters
- `status` (string, optional): One of: `queued`, `scheduled`, `blocked`, `connecting_agent`, `dialing`, `in_progress`, `completed`, `no_answer`, `voicemail`, `failed`.
- `source` (string, optional): What started the call: `api`, `speed_to_lead`, `campaign` or `agent_dial`. | | |---| | `speed_to_lead`
The AI calls back someone who just asked to be contacted. | | `agent_dial`
A GH Live agent dials a number by hand from the workspace. | | `campaign`
The AI works through a client's campaign contact list. | | `api`
The client's own system queued an AI call through the public API or platform MCP server. | One of: `speed_to_lead`, `agent_dial`, `campaign`, `api`.
- `phone_number` (string, optional): Calls to this number, in any common US format. Up to 32 characters.
- `since` (string (date-time), optional): Calls queued at or after this time (ISO 8601).
- `until` (string (date-time), optional): Calls queued before this time (ISO 8601).
- `per_page` (integer, optional): How many items per page, 1 to 100. Minimum 1. Maximum 100.
- `cursor` (string or null, optional): The `meta.next_cursor` (or `meta.prev_cursor`) of the previous page. Up to 1024 characters.
### Responses
- `200`: Paginated set of `OutboundCallResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `outbound:write` scope.
- `422`: Validation error
- `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).
- `source` (string, required): What started the call: your system (`api`), a speed-to-lead callback, a campaign or an agent. | | |---| | `speed_to_lead`
The AI calls back someone who just asked to be contacted. | | `agent_dial`
A GH Live agent dials a number by hand from the workspace. | | `campaign`
The AI works through a client's campaign contact list. | | `api`
The client's own system queued an AI call through the public API or platform MCP server. | One of: `speed_to_lead`, `agent_dial`, `campaign`, `api`.
- `purpose` (string, required): `marketing` needs marketing consent; `informational` covers reminders and follow-ups the person asked for. One of: `marketing`, `informational`.
- `status` (string, required): One of: `queued`, `scheduled`, `blocked`, `connecting_agent`, `dialing`, `in_progress`, `completed`, `no_answer`, `voicemail`, `failed`.
- `contact_name` (string or null, required)
- `phone_number` (string, required)
- `blocked_reason` (string or null, required): Why the call was blocked or delayed, as a code.
- `blocked_reason_message` (string or null, required): Why the call was blocked or delayed, in words.
- `scheduled_for` (string or null, required): When a delayed call is due to ring (calling hours, daily limit).
- `attempts` (integer, required)
- `answered_at` (string or null, required)
- `answered_by` (string or null, required): Who picked up, from answering machine detection: `human`, `machine` or `unknown`. `null` when there was no result (not answered, or an agent's call). One of: `human`, `machine`, `unknown`.
- `voicemail_left_at` (string or null, required): When the AI finished leaving a message on voicemail, under the account's "leave a message" setting.
- `ended_at` (string or null, required)
- `duration_seconds` (integer or null, required)
- `summary` (string or null, optional): The AI's summary of the call. Not included for HIPAA-mode accounts.
- `transcript` (array of objects or null, optional): The conversation, turn by turn. Only on `GET /outbound-calls/{id}`, and never for HIPAA-mode accounts.
- `role` (string, required)
- `text` (string, required)
- `created_at` (string or null, required)
- `links` (object, required)
- `first` (string or null, required)
- `last` (string or null, required)
- `prev` (string or null, required)
- `next` (string or null, required)
- `meta` (object, required)
- `path` (string or null, required): Base path for paginator generated URLs.
- `per_page` (integer, required): Number of items shown per page. Minimum 0.
- `next_cursor` (string or null, required): The "cursor" that points to the next set of items.
- `prev_cursor` (string or null, required): The "cursor" that points to the previous set of items.
### Example request
```bash
curl https://vocenya.com/api/public/v1/outbound-calls \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/outbound-calls', {
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/outbound-calls', [
'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/outbound-calls",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": [
{
"id": 1,
"livemode": true,
"source": "speed_to_lead",
"purpose": "marketing",
"status": "queued",
"contact_name": "Maria Rivera",
"phone_number": "+15125550123",
"blocked_reason": "string",
"blocked_reason_message": "string",
"scheduled_for": "string",
"attempts": 1,
"answered_at": "2026-10-02T14:03:11+00:00",
"answered_by": "human",
"voicemail_left_at": "2026-10-02T14:03:11+00:00",
"ended_at": "2026-10-02T14:03:11+00:00",
"duration_seconds": 1,
"summary": "string",
"transcript": [
{
"role": "string",
"text": "string"
}
],
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"path": "string",
"per_page": 1,
"next_cursor": "string",
"prev_cursor": "string"
}
}
```
## Queue an outbound AI call
`POST https://vocenya.com/api/public/v1/outbound-calls`
Required scope: `outbound:write`
Queue an AI call. Every call is checked against the same rules as calls from the portal:
the number must have AI-call consent on record (marketing consent for `marketing` calls) and
must not be on your or GH's Do Not Call list. Those refusals return 422 with the `decision`.
A call that cannot ring right now (outside 8am to 8pm where the number is, three calls in the
last 24 hours, or already on a call) is still accepted: `decision.retry_at` says when it will
ring. Asking again for a number that is still waiting returns the same call.
With a test key (`vk_test_`) the call is checked exactly the same way (against your test Do Not
Call list and test consents), except the free trial's AI minutes, and is then simulated: it never
rings anyone, moves from `queued` to `in_progress` to `completed` with a sample transcript within
a few seconds, and sends `outbound_call.completed` to your test webhook endpoints.
### Request body
- `phone_number` (string, required): The US number to call. It needs AI-call consent on record and must not be on a Do Not Call list. Up to 32 characters.
- `contact_name` (string or null, optional): Up to 255 characters.
- `purpose` (string or null, optional): `marketing` (the default) needs marketing consent. `informational` is for reminders and follow-ups the person asked for, and the only purpose HIPAA-mode accounts may use. One of: `marketing`, `informational`.
- `context` (array of strings or null, optional): Anything the AI should know for the call, like the service they asked about (up to 20 keys). Up to 20 items.
### Responses
- `202`: Accepted. `decision.allowed` is true when the call can ring now; otherwise `decision.retry_at` says when it will.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `outbound:write` scope.
- `422`: The call can never be placed: `do_not_call`, `no_consent`, `invalid_number`, `outbound_disabled`, `service_inactive`, `terms_not_accepted`, `hipaa_marketing`, `trial_minutes_exhausted` (the free trial's included AI minutes are used up) or `no_receptionist`.
- `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).
- `source` (string, required): What started the call: your system (`api`), a speed-to-lead callback, a campaign or an agent. | | |---| | `speed_to_lead`
The AI calls back someone who just asked to be contacted. | | `agent_dial`
A GH Live agent dials a number by hand from the workspace. | | `campaign`
The AI works through a client's campaign contact list. | | `api`
The client's own system queued an AI call through the public API or platform MCP server. | One of: `speed_to_lead`, `agent_dial`, `campaign`, `api`.
- `purpose` (string, required): `marketing` needs marketing consent; `informational` covers reminders and follow-ups the person asked for. One of: `marketing`, `informational`.
- `status` (string, required): One of: `queued`, `scheduled`, `blocked`, `connecting_agent`, `dialing`, `in_progress`, `completed`, `no_answer`, `voicemail`, `failed`.
- `contact_name` (string or null, required)
- `phone_number` (string, required)
- `blocked_reason` (string or null, required): Why the call was blocked or delayed, as a code.
- `blocked_reason_message` (string or null, required): Why the call was blocked or delayed, in words.
- `scheduled_for` (string or null, required): When a delayed call is due to ring (calling hours, daily limit).
- `attempts` (integer, required)
- `answered_at` (string or null, required)
- `answered_by` (string or null, required): Who picked up, from answering machine detection: `human`, `machine` or `unknown`. `null` when there was no result (not answered, or an agent's call). One of: `human`, `machine`, `unknown`.
- `voicemail_left_at` (string or null, required): When the AI finished leaving a message on voicemail, under the account's "leave a message" setting.
- `ended_at` (string or null, required)
- `duration_seconds` (integer or null, required)
- `summary` (string or null, optional): The AI's summary of the call. Not included for HIPAA-mode accounts.
- `transcript` (array of objects or null, optional): The conversation, turn by turn. Only on `GET /outbound-calls/{id}`, and never for HIPAA-mode accounts.
- `role` (string, required)
- `text` (string, required)
- `created_at` (string or null, required)
- `decision` (object, required)
- `allowed` (boolean, required)
- `reason` (string or null, required)
- `message` (string or null, required)
- `retry_at` (string or null, required)
### Example request
```bash
curl -X POST https://vocenya.com/api/public/v1/outbound-calls \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+15125550123",
"contact_name": "Maria Rivera",
"context": {
"service": "Water heater replacement",
"preferred_time": "mornings"
}
}'
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/outbound-calls', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"phone_number": "+15125550123",
"contact_name": "Maria Rivera",
"context": {
"service": "Water heater replacement",
"preferred_time": "mornings"
}
}),
});
const data = await response.json();
console.log(data);
```
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://vocenya.com/api/public/v1/outbound-calls', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
'json' => [
'phone_number' => '+15125550123',
'contact_name' => 'Maria Rivera',
'context' => [
'service' => 'Water heater replacement',
'preferred_time' => 'mornings',
],
],
]);
$data = json_decode((string) $response->getBody(), true);
print_r($data);
```
```python
import os
import requests
response = requests.post(
"https://vocenya.com/api/public/v1/outbound-calls",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
json={
"phone_number": "+15125550123",
"contact_name": "Maria Rivera",
"context": {
"service": "Water heater replacement",
"preferred_time": "mornings",
},
},
)
print(response.json())
```
### Example response (202)
```json
{
"data": {
"id": 88,
"source": "api",
"purpose": "marketing",
"status": "queued",
"phone_number": "+15125550123"
},
"decision": {
"allowed": false,
"reason": "outside_hours",
"message": "It is outside calling hours (8am to 8pm) where that number is.",
"retry_at": "2026-09-29T08:00:00-05:00"
}
}
```
## Get an outbound call
`GET https://vocenya.com/api/public/v1/outbound-calls/{outboundCall}`
Required scope: `outbound:write`
Check on a call you queued: its status, why it was blocked or delayed and, once it ended, its
transcript (not for HIPAA-mode accounts).
### Path parameters
- `outboundCall` (integer, required): The outbound call id.
### Responses
- `200`: `OutboundCallResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `outbound:write` scope.
- `404`: No record with that id belongs to your account.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `source` (string, required): What started the call: your system (`api`), a speed-to-lead callback, a campaign or an agent. | | |---| | `speed_to_lead`
The AI calls back someone who just asked to be contacted. | | `agent_dial`
A GH Live agent dials a number by hand from the workspace. | | `campaign`
The AI works through a client's campaign contact list. | | `api`
The client's own system queued an AI call through the public API or platform MCP server. | One of: `speed_to_lead`, `agent_dial`, `campaign`, `api`.
- `purpose` (string, required): `marketing` needs marketing consent; `informational` covers reminders and follow-ups the person asked for. One of: `marketing`, `informational`.
- `status` (string, required): One of: `queued`, `scheduled`, `blocked`, `connecting_agent`, `dialing`, `in_progress`, `completed`, `no_answer`, `voicemail`, `failed`.
- `contact_name` (string or null, required)
- `phone_number` (string, required)
- `blocked_reason` (string or null, required): Why the call was blocked or delayed, as a code.
- `blocked_reason_message` (string or null, required): Why the call was blocked or delayed, in words.
- `scheduled_for` (string or null, required): When a delayed call is due to ring (calling hours, daily limit).
- `attempts` (integer, required)
- `answered_at` (string or null, required)
- `answered_by` (string or null, required): Who picked up, from answering machine detection: `human`, `machine` or `unknown`. `null` when there was no result (not answered, or an agent's call). One of: `human`, `machine`, `unknown`.
- `voicemail_left_at` (string or null, required): When the AI finished leaving a message on voicemail, under the account's "leave a message" setting.
- `ended_at` (string or null, required)
- `duration_seconds` (integer or null, required)
- `summary` (string or null, optional): The AI's summary of the call. Not included for HIPAA-mode accounts.
- `transcript` (array of objects or null, optional): The conversation, turn by turn. Only on `GET /outbound-calls/{id}`, and never for HIPAA-mode accounts.
- `role` (string, required)
- `text` (string, required)
- `created_at` (string or null, required)
### Example request
```bash
curl https://vocenya.com/api/public/v1/outbound-calls/88 \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/outbound-calls/88', {
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/outbound-calls/88', [
'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/outbound-calls/88",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"id": 1,
"livemode": true,
"source": "speed_to_lead",
"purpose": "marketing",
"status": "queued",
"contact_name": "Maria Rivera",
"phone_number": "+15125550123",
"blocked_reason": "string",
"blocked_reason_message": "string",
"scheduled_for": "string",
"attempts": 1,
"answered_at": "2026-10-02T14:03:11+00:00",
"answered_by": "human",
"voicemail_left_at": "2026-10-02T14:03:11+00:00",
"ended_at": "2026-10-02T14:03:11+00:00",
"duration_seconds": 1,
"summary": "string",
"transcript": [
{
"role": "string",
"text": "string"
}
],
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
---
# Do Not Call
Numbers your AI never calls. Reading needs `dnc:read`, changes need `dnc:write`. GH's own list is always enforced but not listed.
Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.
- [List Do Not Call entries](#list-do-not-call-entries): `GET /do-not-call`
- [Add a number to the Do Not Call list](#create-do-not-call-entry): `POST /do-not-call`
- [Remove a number from the Do Not Call list](#delete-do-not-call-entry): `DELETE /do-not-call/{entry}`
## List Do Not Call entries
`GET https://vocenya.com/api/public/v1/do-not-call`
Required scope: `dnc:read`
Your Do Not Call list, newest first. Pass `phone_number` to look up one number.
### Query parameters
- `phone_number` (string, optional): Look up one number, in any common US format. Up to 32 characters.
- `source` (string, optional): One of: `opt_out`, `manual`.
- `per_page` (integer, optional): How many items per page, 1 to 100. Minimum 1. Maximum 100.
- `cursor` (string or null, optional): The `meta.next_cursor` (or `meta.prev_cursor`) of the previous page. Up to 1024 characters.
### Responses
- `200`: Paginated set of `DoNotCallEntryResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `dnc:read` scope.
- `422`: Validation error
- `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).
- `phone_number` (string, required)
- `source` (string, required): `opt_out` when the person asked not to be called (permanent), `manual` when you added it. | | |---| | `opt_out`
The person asked not to be called: on an AI call, to a GH Live agent, or to the client. | | `manual`
Added by GH staff or the client. | | `national_registry`
Imported from the FTC National Do Not Call Registry. | One of: `opt_out`, `manual`, `national_registry`.
- `note` (string or null, required)
- `removable` (boolean, required): Whether the entry can be removed. People who asked not to be called never can.
- `created_at` (string or null, required)
- `links` (object, required)
- `first` (string or null, required)
- `last` (string or null, required)
- `prev` (string or null, required)
- `next` (string or null, required)
- `meta` (object, required)
- `path` (string or null, required): Base path for paginator generated URLs.
- `per_page` (integer, required): Number of items shown per page. Minimum 0.
- `next_cursor` (string or null, required): The "cursor" that points to the next set of items.
- `prev_cursor` (string or null, required): The "cursor" that points to the previous set of items.
### Example request
```bash
curl https://vocenya.com/api/public/v1/do-not-call \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/do-not-call', {
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/do-not-call', [
'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/do-not-call",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": [
{
"id": 1,
"livemode": true,
"phone_number": "+15125550123",
"source": "opt_out",
"note": "string",
"removable": true,
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"path": "string",
"per_page": 1,
"next_cursor": "string",
"prev_cursor": "string"
}
}
```
## Add a number to the Do Not Call list
`POST https://vocenya.com/api/public/v1/do-not-call`
Required scope: `dnc:write`
Add a number. Send `opted_out: true` when the person asked not to be called: it becomes a
permanent opt-out and their AI-call consent is revoked. Adding a number already on the list
returns the existing entry with status 200.
### Request body
- `phone_number` (string, required): Up to 32 characters.
- `note` (string or null, optional): Up to 500 characters.
- `opted_out` (boolean, optional): The person asked not to be called. Recorded as a permanent opt-out that can never be removed, and any AI-call consent they gave is revoked.
### Responses
- `200`: The number was already on the list: the existing entry.
- `201`: The entry.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `dnc:write` scope.
- `422`: Validation failed, or the number is not a valid US number (`invalid_number`).
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `phone_number` (string, required)
- `source` (string, required): `opt_out` when the person asked not to be called (permanent), `manual` when you added it. | | |---| | `opt_out`
The person asked not to be called: on an AI call, to a GH Live agent, or to the client. | | `manual`
Added by GH staff or the client. | | `national_registry`
Imported from the FTC National Do Not Call Registry. | One of: `opt_out`, `manual`, `national_registry`.
- `note` (string or null, required)
- `removable` (boolean, required): Whether the entry can be removed. People who asked not to be called never can.
- `created_at` (string or null, required)
### Example request
```bash
curl -X POST https://vocenya.com/api/public/v1/do-not-call \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+15555550123",
"note": "Asked us to stop calling"
}'
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/do-not-call', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"phone_number": "+15555550123",
"note": "Asked us to stop calling"
}),
});
const data = await response.json();
console.log(data);
```
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://vocenya.com/api/public/v1/do-not-call', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
'json' => [
'phone_number' => '+15555550123',
'note' => 'Asked us to stop calling',
],
]);
$data = json_decode((string) $response->getBody(), true);
print_r($data);
```
```python
import os
import requests
response = requests.post(
"https://vocenya.com/api/public/v1/do-not-call",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
json={
"phone_number": "+15555550123",
"note": "Asked us to stop calling",
},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"id": 1,
"livemode": true,
"phone_number": "+15125550123",
"source": "opt_out",
"note": "string",
"removable": true,
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
## Remove a number from the Do Not Call list
`DELETE https://vocenya.com/api/public/v1/do-not-call/{entry}`
Required scope: `dnc:write`
Remove a number you added. People who asked not to be called can never be removed.
### Path parameters
- `entry` (integer, required): The entry id.
### Responses
- `204`: Removed.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The entry is an opt-out and cannot be removed (`opt_out_permanent`).
- `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/do-not-call/57 \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/do-not-call/57', {
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/do-not-call/57', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
]);
echo $response->getStatusCode();
```
```python
import os
import requests
response = requests.delete(
"https://vocenya.com/api/public/v1/do-not-call/57",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.status_code)
```
---
# Blocked callers
Numbers and prefixes whose inbound calls are turned away before your receptionist answers (spam, robocallers). Reading needs `dnc:read`, changes need `dnc:write`.
Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.
- [List blocked callers](#list-blocked-callers): `GET /blocked-callers`
- [Block a caller](#create-blocked-caller): `POST /blocked-callers`
- [Unblock a caller](#delete-blocked-caller): `DELETE /blocked-callers/{entry}`
## List blocked callers
`GET https://vocenya.com/api/public/v1/blocked-callers`
Required scope: `dnc:read`
Your blocked callers, newest first. Pass `pattern` to look up one number or prefix.
### Query parameters
- `pattern` (string, optional): Look up one number (any common US format) or prefix (digits ending in `*`). Up to 32 characters.
- `source` (string, optional): `manual` (you added it), `ai` (the receptionist blocked a spam caller) or `call` (blocked from a call's page). | | |---| | `manual`
Typed in on the blocked callers page or sent through the API. | | `ai`
The AI receptionist judged a call spam and blocked the number. | | `call`
Blocked from a call's page. | One of: `manual`, `ai`, `call`.
- `per_page` (integer, optional): How many items per page, 1 to 100. Minimum 1. Maximum 100.
- `cursor` (string or null, optional): The `meta.next_cursor` (or `meta.prev_cursor`) of the previous page. Up to 1024 characters.
### Responses
- `200`: Paginated set of `BlockedCallerResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `dnc:read` scope.
- `422`: Validation error
- `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).
- `pattern` (string, required): An E.164 number, or a prefix ending in `*` that blocks every number starting with it.
- `source` (string, required): `manual` when you added it, `ai` when the receptionist blocked a spam caller, `call` when blocked from a call's page. | | |---| | `manual`
Typed in on the blocked callers page or sent through the API. | | `ai`
The AI receptionist judged a call spam and blocked the number. | | `call`
Blocked from a call's page. | One of: `manual`, `ai`, `call`.
- `note` (string or null, required)
- `calls_blocked` (integer, required): How many calls this entry has turned away.
- `last_blocked_at` (string or null, required)
- `created_at` (string or null, required)
- `links` (object, required)
- `first` (string or null, required)
- `last` (string or null, required)
- `prev` (string or null, required)
- `next` (string or null, required)
- `meta` (object, required)
- `path` (string or null, required): Base path for paginator generated URLs.
- `per_page` (integer, required): Number of items shown per page. Minimum 0.
- `next_cursor` (string or null, required): The "cursor" that points to the next set of items.
- `prev_cursor` (string or null, required): The "cursor" that points to the previous set of items.
### Example request
```bash
curl https://vocenya.com/api/public/v1/blocked-callers \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/blocked-callers', {
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/blocked-callers', [
'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/blocked-callers",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": [
{
"id": 1,
"livemode": true,
"pattern": "+15125550123",
"source": "manual",
"note": "string",
"calls_blocked": 1,
"last_blocked_at": "2026-10-02T14:03:11+00:00",
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"path": "string",
"per_page": 1,
"next_cursor": "string",
"prev_cursor": "string"
}
}
```
## Block a caller
`POST https://vocenya.com/api/public/v1/blocked-callers`
Required scope: `dnc:write`
Block a number, or every number starting with a prefix (`512555*`). Adding a pattern already on the list
returns the existing entry with status 200.
### Request body
- `pattern` (string, required): A US phone number in any common format, or a prefix of at least four digits ending in `*` to block every number starting with it. Up to 32 characters.
- `note` (string or null, optional): Up to 255 characters.
### Responses
- `200`: The pattern was already on the list: the existing entry.
- `201`: The entry.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `dnc:write` scope.
- `422`: Validation failed, or the pattern is neither a US number nor a prefix (`invalid_pattern`).
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `pattern` (string, required): An E.164 number, or a prefix ending in `*` that blocks every number starting with it.
- `source` (string, required): `manual` when you added it, `ai` when the receptionist blocked a spam caller, `call` when blocked from a call's page. | | |---| | `manual`
Typed in on the blocked callers page or sent through the API. | | `ai`
The AI receptionist judged a call spam and blocked the number. | | `call`
Blocked from a call's page. | One of: `manual`, `ai`, `call`.
- `note` (string or null, required)
- `calls_blocked` (integer, required): How many calls this entry has turned away.
- `last_blocked_at` (string or null, required)
- `created_at` (string or null, required)
### Example request
```bash
curl -X POST https://vocenya.com/api/public/v1/blocked-callers \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pattern": "string",
"note": "Robocaller"
}'
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/blocked-callers', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"pattern": "string",
"note": "Robocaller"
}),
});
const data = await response.json();
console.log(data);
```
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://vocenya.com/api/public/v1/blocked-callers', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
'json' => [
'pattern' => 'string',
'note' => 'Robocaller',
],
]);
$data = json_decode((string) $response->getBody(), true);
print_r($data);
```
```python
import os
import requests
response = requests.post(
"https://vocenya.com/api/public/v1/blocked-callers",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
json={
"pattern": "string",
"note": "Robocaller",
},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"id": 1,
"livemode": true,
"pattern": "+15125550123",
"source": "manual",
"note": "string",
"calls_blocked": 1,
"last_blocked_at": "2026-10-02T14:03:11+00:00",
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
## Unblock a caller
`DELETE https://vocenya.com/api/public/v1/blocked-callers/{entry}`
Required scope: `dnc:write`
### Path parameters
- `entry` (integer, required): The entry id.
### Responses
- `204`: Removed.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `dnc:write` 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/blocked-callers/31 \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/blocked-callers/31', {
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/blocked-callers/31', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
]);
echo $response->getStatusCode();
```
```python
import os
import requests
response = requests.delete(
"https://vocenya.com/api/public/v1/blocked-callers/31",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.status_code)
```
---
# Chats
Chats on your website widget (`channel: web`) and on WhatsApp (`channel: whatsapp`, with the customer's
number in `contact_address`). Reading needs `chats:read`; replying and closing need `chats:write`.
WhatsApp only allows free-form replies within 24 hours of the customer's last message: after that a reply
is refused with `whatsapp_window_closed` (`reply_window_open` tells you beforehand).
Replies you send show in the visitor's widget straight away, from "Team", exactly like a reply typed in
the portal inbox. Replying to a chat the AI is answering takes it over (status `with_owner`), so the AI
stops answering. To build a bot or route chats to your own help desk, subscribe a webhook endpoint to
`chat.message.created` and answer with `POST /chats/{chat}/messages`; skip messages with `via_api: true`,
which are your own.
Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`.
- [List chats](#list-chats): `GET /chats`
- [Get a chat](#get-chat): `GET /chats/{chat}`
- [Reply in a chat](#create-chat-message): `POST /chats/{chat}/messages`
- [Close a chat](#close-chat): `POST /chats/{chat}/close`
## List chats
`GET https://vocenya.com/api/public/v1/chats`
Required scope: `chats:read`
Your website chats, newest first.
### Query parameters
- `status` (string, optional): Who is answering a website chat right now. One of: `ai`, `with_agent`, `with_owner`, `closed`.
- `since` (string (date-time), optional): Chats started at or after this time (ISO 8601).
- `until` (string (date-time), optional): Chats started before this time (ISO 8601).
- `per_page` (integer, optional): How many items per page, 1 to 100. Minimum 1. Maximum 100.
- `cursor` (string or null, optional): The `meta.next_cursor` (or `meta.prev_cursor`) of the previous page. Up to 1024 characters.
### Responses
- `200`: Paginated set of `ChatResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `chats:read` scope.
- `422`: Validation error
- `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).
- `channel` (string, required): Where the chat happens: `web` (the website widget) or `whatsapp`.
- `contact_address` (string or null, required): The customer's WhatsApp number in E.164 on `whatsapp` chats; `null` on website chats.
- `reply_window_open` (boolean or null, required): On `whatsapp` chats, whether WhatsApp's 24-hour window for free-form replies is open now.
- `status` (string, required): `ai` while the AI answers, `with_agent` or `with_owner` after a handoff, then `closed`. One of: `ai`, `with_agent`, `with_owner`, `closed`.
- `page_url` (string or null, required)
- `handoff_summary` (string or null, optional): What the AI passed on when it handed the chat to a person. Not included for HIPAA-mode accounts.
- `handed_off_at` (string or null, required)
- `closed_at` (string or null, required)
- `last_message_at` (string or null, required)
- `lead_id` (integer or null, required): The lead the AI captured in this chat, if any.
- `messages` (array of objects, optional): The conversation. Only on `GET /chats/{id}`, and never for HIPAA-mode accounts.
- `id` (integer, required)
- `livemode` (boolean, required)
- `sender` (string, required)
- `role` (string, required): One of: `visitor`, `ai`, `team`, `system`.
- `author` (string, required)
- `body` (string, required)
- `via_api` (boolean, required)
- `created_at` (string or null, required)
- `created_at` (string or null, required)
- `links` (object, required)
- `first` (string or null, required)
- `last` (string or null, required)
- `prev` (string or null, required)
- `next` (string or null, required)
- `meta` (object, required)
- `path` (string or null, required): Base path for paginator generated URLs.
- `per_page` (integer, required): Number of items shown per page. Minimum 0.
- `next_cursor` (string or null, required): The "cursor" that points to the next set of items.
- `prev_cursor` (string or null, required): The "cursor" that points to the previous set of items.
### Example request
```bash
curl https://vocenya.com/api/public/v1/chats \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/chats', {
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/chats', [
'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/chats",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": [
{
"id": 1,
"livemode": true,
"channel": "web",
"contact_address": "string",
"reply_window_open": true,
"status": "ai",
"page_url": "https://www.example.com/contact",
"handoff_summary": "string",
"handed_off_at": "2026-10-02T14:03:11+00:00",
"closed_at": "2026-10-02T14:03:11+00:00",
"last_message_at": "2026-10-02T14:03:11+00:00",
"lead_id": 1,
"messages": [
{
"id": 1,
"livemode": true,
"sender": "string",
"role": "visitor",
"author": "string",
"body": "string",
"via_api": true,
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"path": "string",
"per_page": 1,
"next_cursor": "string",
"prev_cursor": "string"
}
}
```
## Get a chat
`GET https://vocenya.com/api/public/v1/chats/{chat}`
Required scope: `chats:read`
One chat with its messages. HIPAA-mode accounts get the chat without messages or handoff summary.
### Path parameters
- `chat` (integer, required): The chat id.
### Responses
- `200`: `ChatResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: The key does not have the `chats:read` scope.
- `404`: No record with that id belongs to your account.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `channel` (string, required): Where the chat happens: `web` (the website widget) or `whatsapp`.
- `contact_address` (string or null, required): The customer's WhatsApp number in E.164 on `whatsapp` chats; `null` on website chats.
- `reply_window_open` (boolean or null, required): On `whatsapp` chats, whether WhatsApp's 24-hour window for free-form replies is open now.
- `status` (string, required): `ai` while the AI answers, `with_agent` or `with_owner` after a handoff, then `closed`. One of: `ai`, `with_agent`, `with_owner`, `closed`.
- `page_url` (string or null, required)
- `handoff_summary` (string or null, optional): What the AI passed on when it handed the chat to a person. Not included for HIPAA-mode accounts.
- `handed_off_at` (string or null, required)
- `closed_at` (string or null, required)
- `last_message_at` (string or null, required)
- `lead_id` (integer or null, required): The lead the AI captured in this chat, if any.
- `messages` (array of objects, optional): The conversation. Only on `GET /chats/{id}`, and never for HIPAA-mode accounts.
- `id` (integer, required)
- `livemode` (boolean, required)
- `sender` (string, required)
- `role` (string, required): One of: `visitor`, `ai`, `team`, `system`.
- `author` (string, required)
- `body` (string, required)
- `via_api` (boolean, required)
- `created_at` (string or null, required)
- `created_at` (string or null, required)
### Example request
```bash
curl https://vocenya.com/api/public/v1/chats/205 \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/chats/205', {
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/chats/205', [
'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/chats/205",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"id": 1,
"livemode": true,
"channel": "web",
"contact_address": "string",
"reply_window_open": true,
"status": "ai",
"page_url": "https://www.example.com/contact",
"handoff_summary": "string",
"handed_off_at": "2026-10-02T14:03:11+00:00",
"closed_at": "2026-10-02T14:03:11+00:00",
"last_message_at": "2026-10-02T14:03:11+00:00",
"lead_id": 1,
"messages": [
{
"id": 1,
"livemode": true,
"sender": "string",
"role": "visitor",
"author": "string",
"body": "string",
"via_api": true,
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
## Reply in a chat
`POST https://vocenya.com/api/public/v1/chats/{chat}/messages`
Required scope: `chats:write`
Reply in a live chat as your team. The visitor sees it at once, from "Team". A reply to a chat the AI
is answering takes it over. Limited to 30 replies a minute per key, on top of the overall limit.
```bash
curl -X POST https://vocenya.com/api/public/v1/chats/205/messages \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body": "Thanks for waiting! We have an opening on Thursday at 3pm."}'
```
### Path parameters
- `chat` (integer, required): The chat id.
### Request body
- `body` (string, required): The reply, as plain text. The visitor sees it from "Team". Up to 2000 characters.
### Responses
- `201`: The message that was sent.
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: Website live chat is not part of the plan (`live_chat_unavailable`).
- `404`: No record with that id belongs to your account.
- `409`: The chat has ended (`chat_closed`).
- `422`: A WhatsApp chat whose 24-hour reply window has closed (`whatsapp_window_closed`): follow up with an approved template from the inbox.
- `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).
- `chat_id` (integer, required)
- `sender` (string, required): Who wrote it: `visitor`, `ai`, `agent` (a GH Live agent), `owner` (the client's team or the API) or `system`.
- `role` (string, required): What the widget shows: `visitor`, `ai`, `team` or `system`. One of: `visitor`, `ai`, `team`, `system`.
- `author` (string, required)
- `body` (string, optional): The text. Not included for HIPAA-mode accounts.
- `via_api` (boolean, required): Whether it was sent with the public API.
- `created_at` (string or null, required)
### Example request
```bash
curl -X POST https://vocenya.com/api/public/v1/chats/205/messages \
-H "Authorization: Bearer $VOCENYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": "Thanks for waiting! We have an opening on Thursday at 3pm. Shall I hold it for you?"
}'
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/chats/205/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"body": "Thanks for waiting! We have an opening on Thursday at 3pm. Shall I hold it for you?"
}),
});
const data = await response.json();
console.log(data);
```
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://vocenya.com/api/public/v1/chats/205/messages', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
'json' => [
'body' => 'Thanks for waiting! We have an opening on Thursday at 3pm. Shall I hold it for you?',
],
]);
$data = json_decode((string) $response->getBody(), true);
print_r($data);
```
```python
import os
import requests
response = requests.post(
"https://vocenya.com/api/public/v1/chats/205/messages",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
json={
"body": "Thanks for waiting! We have an opening on Thursday at 3pm. Shall I hold it for you?",
},
)
print(response.json())
```
### Example response (201)
```json
{
"data": {
"id": 1,
"livemode": true,
"chat_id": 205,
"sender": "string",
"role": "team",
"author": "Team",
"body": "string",
"via_api": true,
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
## Close a chat
`POST https://vocenya.com/api/public/v1/chats/{chat}/close`
Required scope: `chats:write`
End a chat. The visitor sees that it ended. Closing a chat that already ended changes nothing.
```bash
curl -X POST https://vocenya.com/api/public/v1/chats/205/close \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
### Path parameters
- `chat` (integer, required): The chat id.
### Responses
- `200`: `ChatResource`
- `401`: The API key is missing, invalid, expired or revoked.
- `403`: Website live chat is not part of the plan (`live_chat_unavailable`).
- `404`: No record with that id belongs to your account.
- `429`: Too many requests for this key. Wait for the `Retry-After` seconds.
### Response attributes (200)
- `data` (object, required)
- `id` (integer, required)
- `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key).
- `channel` (string, required): Where the chat happens: `web` (the website widget) or `whatsapp`.
- `contact_address` (string or null, required): The customer's WhatsApp number in E.164 on `whatsapp` chats; `null` on website chats.
- `reply_window_open` (boolean or null, required): On `whatsapp` chats, whether WhatsApp's 24-hour window for free-form replies is open now.
- `status` (string, required): `ai` while the AI answers, `with_agent` or `with_owner` after a handoff, then `closed`. One of: `ai`, `with_agent`, `with_owner`, `closed`.
- `page_url` (string or null, required)
- `handoff_summary` (string or null, optional): What the AI passed on when it handed the chat to a person. Not included for HIPAA-mode accounts.
- `handed_off_at` (string or null, required)
- `closed_at` (string or null, required)
- `last_message_at` (string or null, required)
- `lead_id` (integer or null, required): The lead the AI captured in this chat, if any.
- `messages` (array of objects, optional): The conversation. Only on `GET /chats/{id}`, and never for HIPAA-mode accounts.
- `id` (integer, required)
- `livemode` (boolean, required)
- `sender` (string, required)
- `role` (string, required): One of: `visitor`, `ai`, `team`, `system`.
- `author` (string, required)
- `body` (string, required)
- `via_api` (boolean, required)
- `created_at` (string or null, required)
- `created_at` (string or null, required)
### Example request
```bash
curl -X POST https://vocenya.com/api/public/v1/chats/205/close \
-H "Authorization: Bearer $VOCENYA_API_KEY"
```
```javascript
const response = await fetch('https://vocenya.com/api/public/v1/chats/205/close', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`,
},
});
const data = await response.json();
console.log(data);
```
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://vocenya.com/api/public/v1/chats/205/close', [
'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
]);
$data = json_decode((string) $response->getBody(), true);
print_r($data);
```
```python
import os
import requests
response = requests.post(
"https://vocenya.com/api/public/v1/chats/205/close",
headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
)
print(response.json())
```
### Example response (200)
```json
{
"data": {
"id": 1,
"livemode": true,
"channel": "web",
"contact_address": "string",
"reply_window_open": true,
"status": "ai",
"page_url": "https://www.example.com/contact",
"handoff_summary": "string",
"handed_off_at": "2026-10-02T14:03:11+00:00",
"closed_at": "2026-10-02T14:03:11+00:00",
"last_message_at": "2026-10-02T14:03:11+00:00",
"lead_id": 1,
"messages": [
{
"id": 1,
"livemode": true,
"sender": "string",
"role": "visitor",
"author": "string",
"body": "string",
"via_api": true,
"created_at": "2026-10-02T14:03:11+00:00"
}
],
"created_at": "2026-10-02T14:03:11+00:00"
}
}
```
---
# 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=
Waiting for its first or next attempt. | | `delivered`
The endpoint answered with a 2xx status. | | `failed`
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"
}
}
```
---
# Webhooks
Get leads, calls, bookings, outbound calls and live chats pushed to your HTTPS endpoint as they happen, signed so you can trust them.
Instead of polling the API, register an HTTPS endpoint and Vocenya sends it a `POST` the moment something happens: a lead is captured, a call ends, an appointment is booked, a chat gets a message. The full list is in [Webhook events](https://vocenya.com/docs/webhook-events).
## Add an endpoint
Add endpoints in the portal under [Developers](https://vocenya.com/app/developers), or with the API using a key with `webhooks:manage`:
```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://example.com/webhooks/vocenya", "events": ["lead.created", "call.completed"]}'
```
The response includes the endpoint's signing `secret` (it starts with `whsec_`). Store it now: it is not shown again. In the portal you can rotate it.
- The URL must be a public `https://` address. Private, local and internal addresses are refused (`unsafe_url`).
- An account can have up to 10 endpoints.
- Subscribing to an event also needs that record's read scope, for example `leads:read` for `lead.created`.
- Endpoints belong to one mode: those created with a test key, or on the Test tab of the Developers page, only receive events from test data.
## What we send
Each delivery is a `POST` with a JSON body:
```json
{
"id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e",
"type": "lead.created",
"livemode": true,
"created_at": "2026-10-02T14:05:40+00:00",
"data": { "lead": { "id": 311, "name": "Jordan Smith", "phone_number": "+15555550123" } }
}
```
`livemode` is `false` for events from [test data](https://vocenya.com/docs/test-mode), which only go to test endpoints. And these headers:
| Header | Value |
| --- | --- |
| `Vocenya-Event` | The event type, like `lead.created`. |
| `Vocenya-Delivery` | The event id, the same as the body's `id`. Use it to ignore duplicates. |
| `Vocenya-Signature` | `t=