# 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=,v1=`. To verify, compute HMAC-SHA256 of `"."` with your endpoint's secret, compare it to `v1` in constant time, and reject timestamps more than 5 minutes old. Answer with any 2xx within 10 seconds. Anything else is retried with backoff (1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h) for up to 8 attempts. Payloads carry ids, statuses, times and contact details, never conversation content, except `chat.message.created`, which carries the message so your system can answer it. HIPAA-mode accounts get ids only. Fetch the full record from the API when you need more. Events: `lead.created`, `call.completed`, `booking.created`, `outbound_call.completed`, and for website live chat `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.handed_off` and `chat.closed`. Live chat events share a `chat` object: `{"id", "status", "page_url", "handed_off_at", "closed_at", "created_at"}`. `chat.message.created` adds `message`: `{"id", "sender", "role", "author", "body", "via_api", "created_at"}`, where `sender` is `visitor`, `ai`, `agent`, `owner` or `system` and `role` is what the widget shows (`visitor`, `ai`, `team` or `system`); skip `via_api: true`, your own replies. `chat.lead_captured` adds `lead` (the `lead.created` fields). `chat.handoff_requested` adds `requested_by` (`visitor` or `ai`) when a person is asked for; `chat.handed_off` also fires when your team takes a chat over by replying. `chat.closed` adds `chat.lead_id` and `chat.message_count`. Example `chat.message.created`: ```json {"id": "9f0c...", "type": "chat.message.created", "created_at": "2026-10-02T14:03:30+00:00", "data": {"chat": {"id": 205, "status": "ai", "page_url": "https://www.example.com/contact", "handed_off_at": null, "closed_at": null, "created_at": "2026-10-02T14:03:11+00:00"}, "message": {"id": 4410, "sender": "visitor", "role": "visitor", "author": "Visitor", "body": "Hi, do you have any openings this week?", "via_api": false, "created_at": "2026-10-02T14:03:30+00:00"}}} ``` The same event for a HIPAA-mode account: `{"chat": {"id": 205}, "message": {"id": 4410}}`. Base URL: `https://vocenya.com/api/public/v1`. Authenticate with `Authorization: Bearer $VOCENYA_API_KEY`. - [List webhook endpoints](#list-webhook-endpoints): `GET /webhook-endpoints` - [Create a webhook endpoint](#create-webhook-endpoint): `POST /webhook-endpoints` - [Delete a webhook endpoint](#delete-webhook-endpoint): `DELETE /webhook-endpoints/{webhookEndpoint}` - [Send a test event](#test-webhook-endpoint): `POST /webhook-endpoints/{webhookEndpoint}/test` ## List webhook endpoints `GET https://vocenya.com/api/public/v1/webhook-endpoints` Required scope: `webhooks:manage` ### Responses - `200`: Array of `WebhookEndpointResource` - `401`: The API key is missing, invalid, expired or revoked. - `403`: The key does not have the `webhooks:manage` scope. - `429`: Too many requests for this key. Wait for the `Retry-After` seconds. ### Response attributes (200) - `data` (array of objects, required) - `id` (integer, required) - `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key). - `url` (string, required) - `description` (string or null, required) - `events` (array of strings, required): The events sent to this endpoint. One of: `lead.created`, `call.completed`, `booking.created`, `chat.handed_off`, `outbound_call.completed`, `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.closed`. - `enabled` (boolean, required) - `created_at` (string or null, required) ### Example request ```bash curl https://vocenya.com/api/public/v1/webhook-endpoints \ -H "Authorization: Bearer $VOCENYA_API_KEY" ``` ```javascript const response = await fetch('https://vocenya.com/api/public/v1/webhook-endpoints', { headers: { Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`, }, }); const data = await response.json(); console.log(data); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://vocenya.com/api/public/v1/webhook-endpoints', [ 'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')], ]); $data = json_decode((string) $response->getBody(), true); print_r($data); ``` ```python import os import requests response = requests.get( "https://vocenya.com/api/public/v1/webhook-endpoints", headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"}, ) print(response.json()) ``` ### Example response (200) ```json { "data": [ { "id": 1, "livemode": true, "url": "https://hooks.example.com/vocenya", "description": "string", "events": [ "lead.created" ], "enabled": true, "created_at": "2026-10-02T14:03:11+00:00" } ] } ``` ## Create a webhook endpoint `POST https://vocenya.com/api/public/v1/webhook-endpoints` Required scope: `webhooks:manage` Add an endpoint. The response includes its signing `secret`: store it now, it is not shown again. ### Request body - `url` (string (uri), required): A public HTTPS URL. Up to 2048 characters. - `events` (array of strings, required): The events to send, at least one. The key needs the read scope of each event's record: `lead.created` needs `leads:read`, `call.completed` needs `calls:read`, `booking.created` needs `bookings:read`, `outbound_call.completed` needs `outbound:write`, every `chat.*` event needs `chats:read`, and `chat.lead_captured` also needs `leads:read`. One of: `lead.created`, `call.completed`, `booking.created`, `chat.handed_off`, `outbound_call.completed`, `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.closed`. At least 1 items. - `description` (string or null, optional): Up to 255 characters. ### Responses - `201`: The endpoint and its signing secret. - `401`: The API key is missing, invalid, expired or revoked. - `403`: The key lacks the read scope of a subscribed event, for example `leads:read` for `lead.created` (`missing_scope`). - `422`: Validation failed, the URL is not a public HTTPS address (`unsafe_url`), or the account already has 10 endpoints (`too_many_endpoints`). - `429`: Too many requests for this key. Wait for the `Retry-After` seconds. ### Response attributes (201) - `data` (object, required) - `id` (integer, required) - `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key). - `url` (string, required) - `description` (string or null, required) - `events` (array of strings, required): The events sent to this endpoint. One of: `lead.created`, `call.completed`, `booking.created`, `chat.handed_off`, `outbound_call.completed`, `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.closed`. - `enabled` (boolean, required) - `created_at` (string or null, required) - `secret` (string, required) ### Example request ```bash curl -X POST https://vocenya.com/api/public/v1/webhook-endpoints \ -H "Authorization: Bearer $VOCENYA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/vocenya", "events": [ "lead.created" ], "description": "Our CRM" }' ``` ```javascript const response = await fetch('https://vocenya.com/api/public/v1/webhook-endpoints', { method: 'POST', headers: { Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "url": "https://hooks.example.com/vocenya", "events": [ "lead.created" ], "description": "Our CRM" }), }); const data = await response.json(); console.log(data); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://vocenya.com/api/public/v1/webhook-endpoints', [ 'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')], 'json' => [ 'url' => 'https://hooks.example.com/vocenya', 'events' => [ 'lead.created', ], 'description' => 'Our CRM', ], ]); $data = json_decode((string) $response->getBody(), true); print_r($data); ``` ```python import os import requests response = requests.post( "https://vocenya.com/api/public/v1/webhook-endpoints", headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"}, json={ "url": "https://hooks.example.com/vocenya", "events": [ "lead.created", ], "description": "Our CRM", }, ) print(response.json()) ``` ### Example response (201) ```json { "data": { "id": 3, "url": "https://hooks.example.com/vocenya", "description": "Our CRM", "events": [ "lead.created" ], "enabled": true, "created_at": "2026-09-27T15:04:05+00:00" }, "secret": "whsec_4hW0cB1m9GqS7pXvL2eYt8Kz3nRd6FjA5uVo0iQs" } ``` ## Delete a webhook endpoint `DELETE https://vocenya.com/api/public/v1/webhook-endpoints/{webhookEndpoint}` Required scope: `webhooks:manage` Delete an endpoint. Queued deliveries to it are dropped. ### Path parameters - `webhookEndpoint` (integer, required): The endpoint id. ### Responses - `204`: Deleted. - `401`: The API key is missing, invalid, expired or revoked. - `403`: The key does not have the `webhooks:manage` scope. - `404`: No record with that id belongs to your account. - `429`: Too many requests for this key. Wait for the `Retry-After` seconds. ### Example request ```bash curl -X DELETE https://vocenya.com/api/public/v1/webhook-endpoints/3 \ -H "Authorization: Bearer $VOCENYA_API_KEY" ``` ```javascript const response = await fetch('https://vocenya.com/api/public/v1/webhook-endpoints/3', { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.VOCENYA_API_KEY}`, }, }); console.log(response.status); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('DELETE', 'https://vocenya.com/api/public/v1/webhook-endpoints/3', [ 'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')], ]); echo $response->getStatusCode(); ``` ```python import os import requests response = requests.delete( "https://vocenya.com/api/public/v1/webhook-endpoints/3", headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"}, ) print(response.status_code) ``` ## Send a test event `POST https://vocenya.com/api/public/v1/webhook-endpoints/{webhookEndpoint}/test` Required scope: `webhooks:manage` Send a signed `webhook.test` event to this endpoint, with `{"message": "..."}` as its data. Pass `event` to get a sample of a real event instead (for example `chat.message.created`), with made-up data and `"sample": true`, cut down to ids for HIPAA-mode accounts like the real thing. It is delivered in the background like any other event, even when the endpoint is paused. ```bash curl -X POST https://vocenya.com/api/public/v1/webhook-endpoints/3/test \ -H "Authorization: Bearer $VOCENYA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event": "chat.message.created"}' ``` ### Path parameters - `webhookEndpoint` (integer, required): The endpoint id. ### Request body - `event` (string or null, optional): Send a sample of this event (made-up data plus `"sample": true`) instead of `webhook.test`. One of: `lead.created`, `call.completed`, `booking.created`, `chat.handed_off`, `outbound_call.completed`, `chat.started`, `chat.message.created`, `chat.lead_captured`, `chat.handoff_requested`, `chat.closed`. ### Responses - `202`: The queued delivery. - `401`: The API key is missing, invalid, expired or revoked. - `403`: The key does not have the `webhooks:manage` scope. - `404`: No record with that id belongs to your account. - `422`: Validation error - `429`: Too many requests for this key. Wait for the `Retry-After` seconds. ### Response attributes (202) - `data` (object, required) - `id` (integer, required) - `livemode` (boolean, required): `true` for live data, `false` for test data (created with a `vk_test_` key). - `event_id` (string, required): The event id, also sent as the `Vocenya-Delivery` header. Use it to ignore duplicates. - `event` (string, required) - `webhook_endpoint_id` (integer, required) - `status` (string, required): | | |---| | `pending`
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=,v1=`. See below. | | `User-Agent` | `Vocenya-Webhooks/1.0` | Payloads carry ids, statuses, times and contact details, never conversation content (summaries, transcripts, lead reasons), with one exception: `chat.message.created` carries the message so your system can answer it. HIPAA-mode accounts receive ids only. Fetch the full record from the API when you need more. ## Verify the signature Always check `Vocenya-Signature` before trusting a delivery. The signature is an HMAC-SHA256, keyed with your endpoint secret, of the timestamp, a dot and the **raw** request body: ```text v1 = hex(HMAC_SHA256(secret, ".")) ``` To verify: 1. Split the header on `,` and read `t` and `v1`. 2. Reject the delivery if `t` is more than 300 seconds (5 minutes) from your clock. This stops replays. 3. Compute the HMAC of `t + "." + raw body` with your secret and compare it to `v1` in constant time. Use the raw body exactly as received. Parsing the JSON and serialising it again changes the bytes and breaks the signature. ```javascript import crypto from 'node:crypto'; import express from 'express'; const app = express(); const secret = process.env.VOCENYA_WEBHOOK_SECRET; function verifySignature(rawBody, header) { const parts = Object.fromEntries( header.split(',').map((pair) => pair.trim().split('=')), ); if (!/^\d+$/.test(parts.t ?? '') || !parts.v1) return false; if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; const expected = crypto .createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`) .digest('hex'); return expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); } app.post('/webhooks/vocenya', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body.toString('utf8'); if (!verifySignature(rawBody, req.get('Vocenya-Signature') ?? '')) { return res.status(400).send('Invalid signature'); } const event = JSON.parse(rawBody); // Handle event.type here; skip event.id values you have already processed. res.sendStatus(200); }); ``` ```php bool: parts = dict(pair.strip().split("=", 1) for pair in header.split(",") if "=" in pair) timestamp, signature = parts.get("t", ""), parts.get("v1", "") if not timestamp.isdigit() or not signature: return False if abs(time.time() - int(timestamp)) > 300: return False expected = hmac.new(SECRET.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) @app.post("/webhooks/vocenya") def vocenya_webhook(): if not verify_signature(request.get_data(), request.headers.get("Vocenya-Signature", "")): abort(400) event = request.get_json() # Handle event["type"] here; skip event["id"] values you have already processed. return "", 200 ``` ```ruby require "json" require "openssl" require "sinatra" SECRET = ENV.fetch("VOCENYA_WEBHOOK_SECRET") def valid_signature?(raw_body, header) parts = header.split(",").map { |pair| pair.strip.split("=", 2) }.to_h timestamp, signature = parts["t"].to_s, parts["v1"].to_s return false unless timestamp.match?(/\A\d+\z/) && !signature.empty? return false if (Time.now.to_i - timestamp.to_i).abs > 300 expected = OpenSSL::HMAC.hexdigest("SHA256", SECRET, "#{timestamp}.#{raw_body}") Rack::Utils.secure_compare(expected, signature) end post "/webhooks/vocenya" do raw_body = request.body.read halt 400, "Invalid signature" unless valid_signature?(raw_body, request.env["HTTP_VOCENYA_SIGNATURE"].to_s) event = JSON.parse(raw_body) # Handle event["type"] here; skip event["id"] values you have already processed. status 200 end ``` ## Respond quickly Answer with any `2xx` status within **10 seconds**. Do the real work afterwards (in a queue or background job), so slow processing never causes a timeout. Redirects are not followed: point the endpoint at its final URL. ## Retries Anything other than a `2xx` within 10 seconds (an error status, a timeout or a connection error) is retried. A delivery is attempted up to **8 times**, waiting 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h between attempts, then marked failed. Every attempt sends the same body and event id with a fresh signature timestamp. Because a delivery can arrive more than once, make your handler idempotent: record each `id` you process and skip repeats. Deliveries can also arrive out of order; use `created_at` and the record's own fields, not arrival order. ## Test your endpoint Send a signed test event from the portal, or with the API: ```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"}' ``` Without `event` you get a `webhook.test` event with `{"message": "..."}` as its data. With `event`, you get a sample of that real event with made-up data and `"sample": true`. Test deliveries are signed and retried like any other. --- # Webhook events Every webhook event Vocenya sends, the scope each one needs and an example payload for each. Subscribe an endpoint to the events you need. Each event needs your key's `webhooks:manage` scope plus the read scope of its record. All events share the envelope `{"id", "type", "livemode", "created_at", "data"}` described in [Webhooks](https://vocenya.com/docs/webhooks); `data` differs per event. | Event | When it is sent | Scopes needed to subscribe | | --- | --- | --- | | [`lead.created`](#lead-created) | A lead was captured | `leads:read` | | [`call.completed`](#call-completed) | An inbound call ended | `calls:read` | | [`booking.created`](#booking-created) | An appointment was booked | `bookings:read` | | [`chat.handed_off`](#chat-handed-off) | A website chat was handed to a person | `chats:read` | | [`outbound_call.completed`](#outbound-call-completed) | An outbound call ended | `outbound:write` | | [`chat.started`](#chat-started) | A visitor started a website chat | `chats:read` | | [`chat.message.created`](#chat-message-created) | A website chat got a new message | `chats:read` | | [`chat.lead_captured`](#chat-lead-captured) | A website chat captured a lead | `chats:read`, `leads:read` | | [`chat.handoff_requested`](#chat-handoff-requested) | The visitor or the AI asked for a person in a website chat | `chats:read` | | [`chat.closed`](#chat-closed) | A website chat was closed | `chats:read` | New event types may be added within the API version, so ignore types you do not handle. ## Call events `call.completed` carries the call's `id`, `direction`, `status`, numbers, times, `duration_seconds` and `spam`: `true` when the receptionist judged the call unwanted (no lead is created for it). Calls from numbers on your [blocked callers list](https://vocenya.com/docs/reference/blocked-callers) never reach anyone and send no event. ## Live chat events Every `chat.*` event carries the same `chat` object: `id`, `status`, `page_url`, `handed_off_at`, `closed_at` and `created_at`. - `chat.message.created` adds `message`. `sender` is who wrote it (`visitor`, `ai`, `agent`, `owner` or `system`), `role` is how the widget shows it (`visitor`, `ai`, `team` or `system`) and `via_api` is `true` for replies sent with the API. To build a bot, answer visitor messages with `POST /chats/{chat}/messages` and skip `via_api: true`, which are your own. - `chat.lead_captured` adds `lead`, with the same fields as `lead.created`. - `chat.handoff_requested` adds `requested_by` (`visitor` or `ai`) when a person is asked for. The chat is then `with_agent` (a GH Live agent has it) or `with_owner` (waiting for your team). - `chat.handed_off` also fires when your team takes a chat over by replying. - `chat.closed` adds `chat.lead_id` and `chat.message_count`. HIPAA-mode accounts receive ids only, for example `{"chat": {"id": 205}, "message": {"id": 4410}}`. ## Example payloads These are the exact samples test deliveries send (a test delivery adds `"sample": true` to `data`). ### lead.created A lead was captured. Subscribing needs `leads:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "lead.created", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "lead": { "id": 311, "call_id": 1042, "chat_conversation_id": null, "outbound_call_id": null, "name": "Jordan Smith", "phone_number": "+15555550123", "email": "jordan@example.com", "urgency": null, "custom_fields": [], "created_at": "2026-10-02T14:05:40+00:00" } } } ``` ### call.completed An inbound call ended. Subscribing needs `calls:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "call.completed", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "call": { "id": 1042, "direction": "inbound", "status": "completed", "from_number": "+15555550123", "to_number": "+15555550199", "started_at": "2026-10-02T14:00:00+00:00", "ended_at": "2026-10-02T14:03:25+00:00", "duration_seconds": 205, "spam": false } } } ``` ### booking.created An appointment was booked. Subscribing needs `bookings:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "booking.created", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "booking": { "id": 88, "call_id": 1042, "outbound_call_id": null, "provider": "google_calendar", "starts_at": "2026-10-05T15:30:00+00:00", "duration_minutes": 30, "name": "Jordan Smith", "phone_number": "+15555550123" } } } ``` ### chat.handed_off A website chat was handed to a person. Subscribing needs `chats:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "chat.handed_off", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "chat": { "id": 205, "status": "with_owner", "page_url": "https://www.example.com/contact", "handed_off_at": "2026-10-02T14:06:00+00:00", "closed_at": null, "created_at": "2026-10-02T14:03:11+00:00" } } } ``` ### outbound_call.completed An outbound call ended. Subscribing needs `outbound:write`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "outbound_call.completed", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "outbound_call": { "id": 57, "campaign_id": null, "source": "api", "purpose": "informational", "status": "completed", "contact_name": "Jordan Smith", "phone_number": "+15555550123", "blocked_reason": null, "answered_at": "2026-10-02T14:06:02+00:00", "answered_by": "human", "ended_at": "2026-10-02T14:08:40+00:00", "duration_seconds": 158 } } } ``` ### chat.started A visitor started a website chat. Subscribing needs `chats:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "chat.started", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "chat": { "id": 205, "status": "ai", "page_url": "https://www.example.com/contact", "handed_off_at": null, "closed_at": null, "created_at": "2026-10-02T14:03:11+00:00" } } } ``` ### chat.message.created A website chat got a new message. Subscribing needs `chats:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "chat.message.created", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "chat": { "id": 205, "status": "ai", "page_url": "https://www.example.com/contact", "handed_off_at": null, "closed_at": null, "created_at": "2026-10-02T14:03:11+00:00" }, "message": { "id": 4410, "sender": "visitor", "role": "visitor", "author": "Visitor", "body": "Hi, do you have any openings this week?", "via_api": false, "created_at": "2026-10-02T14:03:30+00:00" } } } ``` ### chat.lead_captured A website chat captured a lead. Subscribing needs `chats:read` and `leads:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "chat.lead_captured", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "chat": { "id": 205, "status": "ai", "page_url": "https://www.example.com/contact", "handed_off_at": null, "closed_at": null, "created_at": "2026-10-02T14:03:11+00:00" }, "lead": { "id": 311, "call_id": null, "chat_conversation_id": 205, "outbound_call_id": null, "name": "Jordan Smith", "phone_number": "+15555550123", "email": "jordan@example.com", "urgency": null, "custom_fields": [], "created_at": "2026-10-02T14:05:40+00:00" } } } ``` ### chat.handoff_requested The visitor or the AI asked for a person in a website chat. Subscribing needs `chats:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "chat.handoff_requested", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "chat": { "id": 205, "status": "with_owner", "page_url": "https://www.example.com/contact", "handed_off_at": "2026-10-02T14:06:00+00:00", "closed_at": null, "created_at": "2026-10-02T14:03:11+00:00" }, "requested_by": "visitor" } } ``` ### chat.closed A website chat was closed. Subscribing needs `chats:read`. ```json { "id": "9f0c6a1e-5b7d-4c1a-9e8f-2d3b4a5c6d7e", "type": "chat.closed", "livemode": true, "created_at": "2026-10-02T14:03:30+00:00", "data": { "chat": { "id": 205, "status": "closed", "page_url": "https://www.example.com/contact", "handed_off_at": "2026-10-02T14:06:00+00:00", "closed_at": "2026-10-02T14:20:12+00:00", "created_at": "2026-10-02T14:03:11+00:00", "lead_id": 311, "message_count": 14 } } } ``` --- # Live chat widget Add the Vocenya website live chat with one script tag, control it with the JavaScript API, and answer chats from your own systems. The Vocenya live chat puts your AI receptionist on your website: it answers visitors, captures leads and hands the chat to a person when it should. Installing it is one script tag with your business's public **site key**, found in the portal under [Live chat → Install](https://vocenya.com/app/live-chat). The instructions below are the same ones Vocenya gives to AI site builders such as Lovable, Bolt, v0 and Cursor. Each business also gets its own copy with its key filled in, at `/chat/{site key}/install.md`, and the generic version is at [https://vocenya.com/developers/chat-install.md](https://vocenya.com/developers/chat-install.md). The portal's install page can copy a ready-made prompt or open it in ChatGPT or Claude for you. ## What to add Add this script tag once, so it loads on every page, just before the closing `` tag (or the framework equivalent below): ```html ``` Replace `pk_YOUR_SITE_KEY` with the site key from the Vocenya portal (Live chat → Install). Each business has its own key. The chat bubble then appears in the corner of every page. Nothing else is required. ## Rules - Load the script once, site-wide, in the shared layout or template. Not per page and not twice. - Load it from https://vocenya.com/api/chat/widget.js exactly. Do not download, bundle, self-host, inline, minify or proxy the script: it is updated on our side. - Do not change, remove or invent the `data-key` value. - Keep `async` and do not add `type="module"`. - It is browser-only. Keep it out of server-only code paths (server components' logic, API routes, SSR data loaders, edge functions). Never call `window.Vocenya` during server rendering; call it only in the browser, from event handlers or effects. - Do not install an npm package for it: there is none. Do not add a chat component of your own. - If the site sends a Content Security Policy, add the sources below rather than loosening the policy any further. ## Where it goes, by framework ### Plain HTML or any server-rendered template Paste the tag just before `` in the template every page shares (footer include, base layout, `theme.liquid`, etc.). ### Next.js (App Router) In the root layout `app/layout.tsx`, use `next/script` with `strategy="afterInteractive"`, inside ``: ```tsx import Script from 'next/script'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ``` ### Remix and React Router (framework mode) In `app/root.tsx`, inside `` next to ``: ```tsx ``` ### Webflow, Framer, Wix, Squarespace and other site builders Use the site-wide custom code setting, not a page embed: - Webflow: Site settings → Custom code → Footer code. Publish. - Framer: Site settings → General → Custom code → End of tag. Publish. - Wix: Settings → Custom code → Add custom code, All pages, Load once, Body - end. - Squarespace: Settings → Advanced → Code injection → Footer. - WordPress: use the Vocenya Chat plugin from the Vocenya portal, or the theme footer. ## Content Security Policy Only if the site sets a Content-Security-Policy (header or meta tag), add these sources to the existing directives: ``` script-src https://vocenya.com connect-src https://vocenya.com wss://ws.vocenya.com img-src https://vocenya.com frame-src https://vocenya.com ``` `connect-src` covers the chat API and the live-reply websocket. `frame-src` is only needed if you also embed the hosted chat page in an iframe. ## Optional: the JavaScript API Only if asked. To call the chat before the script has loaded, add this stub above the script tag; calls are queued and replayed: ```html ``` - `Vocenya('open')`, `Vocenya('close')`, `Vocenya('toggle')`: control the chat window. - `Vocenya('identify', { name, email, phone })`: prefill a signed-in visitor so the team sees who they are. - `Vocenya('on', 'message' | 'open' | 'close' | 'lead', callback)` and `Vocenya('off', event, callback)`: listen for chat events. A "Chat with us" button: ```html ``` Identify a logged-in user in React (browser only): ```tsx useEffect(() => { if (user) { window.Vocenya?.('identify', { name: user.name, email: user.email }); } }, [user]); ``` In TypeScript, declare it once: `declare global { interface Window { Vocenya?: (...args: unknown[]) => void } }`. ## Allowed domains The chat only runs on the domains listed under Live chat → Install in the Vocenya portal. Make sure the site's live domain is on that list; preview or staging domains (for example `*.lovable.app` or `*.vercel.app`) must be added too if the chat should work there. ## Check it worked 1. Publish or deploy the site. 2. Open the live site in a private window: a chat bubble appears in the corner. Click it and send a test message. 3. No bubble? Open the browser console: look for a blocked script or connection (Content Security Policy) or a domain that is not allowed. 4. Within a few minutes the Vocenya portal shows the chat as "Installed". ## When you are done Tell the owner which file you changed and confirm the script tag is unchanged. Docs: https://vocenya.com/developers/chat-install.md ## Answer chats from your own systems The chat is also part of the API, so a bot or your help desk can take part: 1. Subscribe a [webhook endpoint](https://vocenya.com/docs/webhooks) to `chat.message.created` (scope `chats:read`). 2. Reply with [`POST /chats/{chat}/messages`](https://vocenya.com/docs/reference/chats#create-chat-message) (scope `chats:write`). Replies show in the visitor's widget straight away, from "Team". Replying to a chat the AI is answering takes it over, so the AI stops answering. 3. Skip messages with `via_api: true`: they are your own replies. 4. Close the chat with [`POST /chats/{chat}/close`](https://vocenya.com/docs/reference/chats#close-chat) when you are done. Replies are limited to 30 a minute per key. See the [Chats reference](https://vocenya.com/docs/reference/chats) and the [live chat events](https://vocenya.com/docs/webhook-events#live-chat-events). ## Channels: website and WhatsApp Chats come from two channels, and the API treats them the same way. Every chat and every `chat.*` webhook payload has a `channel`: - `web`: the website widget. `page_url` is the page the visitor was on, and `contact_address` is `null`. - `whatsapp`: a customer messaging the business's WhatsApp number. `contact_address` is the customer's number in E.164 (for example `+15550102030`), and `page_url` is `null`. WhatsApp only allows free-form replies within 24 hours of the customer's last message. `reply_window_open` on a WhatsApp chat tells you whether that window is open. Outside it, `POST /chats/{chat}/messages` is refused with `422` and code `whatsapp_window_closed`, plus a `hint`. Follow up with an approved template from the Vocenya inbox instead. Templates only go to customers who opted in to WhatsApp messages and have not replied STOP. WhatsApp is not available to HIPAA-mode accounts. --- # MCP servers Connect Claude, ChatGPT, Cursor and other AI assistants to your Vocenya account and to these docs with the Model Context Protocol. Vocenya runs three [Model Context Protocol](https://modelcontextprotocol.io) servers over streamable HTTP. Point an AI assistant at them and it can work with your account or answer questions from these docs, without you writing any code. | Server | URL | Authentication | What it does | | --- | --- | --- | --- | | Platform | `https://vocenya.com/mcp/platform` | API key | Your own account: calls, leads, bookings, chats, outbound calls and the Do Not Call list. | | Docs | `https://vocenya.com/mcp/docs` | None | Read-only search over these developer docs and the API reference. | | Site | `https://vocenya.com/mcp/site` | None | Read-only facts about Vocenya: pricing, products, industries and guides. | ## Platform MCP server The platform server works on one business account: the one whose API key you connect with (or that an owner chose when connecting an app with [OAuth](#connect-with-oauth-no-key)). It uses the same [API keys and scopes](https://vocenya.com/docs/authentication) as the REST API, and shares the key's [rate limit](https://vocenya.com/docs/rate-limits). Each tool checks its own scope, so a key with only `leads:read` can list leads but not queue calls. | Tool | What it does | | --- | --- | | `list_calls` | List phone calls the AI receptionist, GH Live agents or the team handled for this business, newest first. Filter by when the call started, its outcome (completed, missed, failed, blocked, ringing, in_progress), direction, and whether it was spam. Returns ids, numbers, timestamps, duration and, unless the account is in HIPAA mode, the AI summary. Needs the calls:read scope. | | `get_call` | Get one phone call by id, with the leads captured on it. Unless the account is in HIPAA mode it also returns the AI summary and the transcript. Needs the calls:read scope. | | `list_leads` | List leads (callers, chat visitors and form inquiries who left their details) for this business, newest first. Filter by phone number, email or when the lead was captured. Needs the leads:read scope. | | `get_lead` | Get one lead by id: contact details, the reason they got in touch, urgency, and the call, chat or outbound call it came from. Needs the leads:read scope. | | `create_lead` | Add a lead to this business, for example an inquiry from another system. It shows up in the Vocenya portal and syncs to a connected CRM. To have the AI call the person back right away (speed-to-lead), set callback_consent_text to the exact wording the person agreed to; that also needs the outbound:write scope, and the call still follows Do Not Call, consent and calling-hours rules. Needs the leads:write scope. | | `list_bookings` | List appointments the AI booked into the business calendar, most recently booked first. Filter by appointment time. Needs the bookings:read scope. | | `queue_outbound_call` | Ask the AI receptionist to call a person who has given consent for AI calls. Every call goes through the compliance check first: numbers on a Do Not Call list, numbers without AI-call consent on record, HIPAA marketing calls, accounts without outbound calling and trial accounts whose included AI minutes are used up are refused with a reason. Calls outside 8am-8pm local time, over the daily limit or while the number is already on a call are queued and ring later; the result says when. Nothing can bypass these rules. Needs the outbound:write scope. | | `add_do_not_call` | Add a phone number to this business's Do Not Call list so the AI never calls it. Set opted_out when the person themselves asked not to be called: that is recorded permanently and revokes their consent. Needs the dnc:write scope. | | `block_caller` | Block a phone number, or every number starting with a prefix (like 512555*), from calling this business: their inbound calls are turned away before the AI receptionist answers. For spam and robocallers; use add_do_not_call for numbers the business must not call. Needs the dnc:write scope. | | `list_chats` | List website live chat conversations, newest first: who is handling each one (ai, with_agent, with_owner or closed), the page it started on, the lead it produced and, unless the account is in HIPAA mode, the handoff summary. Needs the chats:read scope. | Outbound calls queued over MCP follow every [outbound rule](https://vocenya.com/docs/outbound-calls). Accounts in HIPAA mode never return transcripts, AI summaries or chat messages. Create a key for your assistant under [Developers](https://vocenya.com/app/developers), with only the scopes it needs, and set it as `VOCENYA_API_KEY`. ### Claude Code ```bash claude mcp add --transport http vocenya https://vocenya.com/mcp/platform \ --header "Authorization: Bearer $VOCENYA_API_KEY" ``` ### Cursor In `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json { "mcpServers": { "vocenya": { "url": "https://vocenya.com/mcp/platform", "headers": { "Authorization": "Bearer ${env:VOCENYA_API_KEY}" } } } } ``` ### Claude Desktop Claude Desktop reaches servers that need a header through the `mcp-remote` bridge. In `claude_desktop_config.json`: ```json { "mcpServers": { "vocenya": { "command": "npx", "args": ["-y", "mcp-remote", "https://vocenya.com/mcp/platform", "--header", "Authorization:${VOCENYA_AUTH}"], "env": { "VOCENYA_AUTH": "Bearer paste-your-key-here" } } } } ``` ### Other clients Any MCP client that supports streamable HTTP and custom headers works: use the URL above and send `Authorization: Bearer `. ### Connect with OAuth (no key) Clients that sign in with OAuth, such as Claude connectors, can connect without a key: add `https://vocenya.com/mcp/platform` as a custom connector. The client registers itself, then sends an account owner to a Vocenya sign-in and consent screen to choose the account, live or test data, and the permissions (the same scopes as API keys). The app's access works exactly like a key with those scopes, including HIPAA mode. Owners see and disconnect connected apps under [Developers](https://vocenya.com/app/developers). The server supports OAuth 2.1 authorization code with PKCE (S256) and dynamic client registration. A request without a credential gets `401` with a `WWW-Authenticate` header pointing at the protected resource metadata. ## Docs MCP server The docs server lets an assistant look things up in these docs while it writes your integration, so it uses real endpoints, parameters and limits instead of guessing. It is public and read-only, needs no key and is limited to 60 requests a minute per IP address. Its answers are generated from the same source as these pages, so they always match. | Tool | What it does | | --- | --- | | `search_docs` | Search the Vocenya developer docs: guides (authentication, webhooks, pagination, outbound calling rules, live chat, MCP) and every API endpoint. Returns the best matches with their page URL; read one with get_doc_page or get_endpoint. | | `get_doc_page` | Get one page of the Vocenya developer docs as Markdown, exactly as published. Slugs: "index", a guide like "quickstart", "authentication", "webhooks", "webhook-events", "outbound-calls", "live-chat" or "mcp", "reference" for the API overview, or "reference/leads" for a resource's endpoints. Use search_docs to find a slug. | | `list_endpoints` | List the endpoints of the Vocenya REST API with their method, path, summary and required scope, grouped by resource. Pass a tag (resource) like "Leads" or "Webhooks" to list one resource. Get the details of one with get_endpoint. | | `get_endpoint` | Get one Vocenya API endpoint in full, as Markdown: its scope, path and query parameters, request body, responses, response attributes and code samples in cURL, Node, PHP and Python. Example: method "POST", path "/leads". | ### Claude Code ```bash claude mcp add --transport http vocenya-docs https://vocenya.com/mcp/docs ``` ### Cursor ```json { "mcpServers": { "vocenya-docs": { "url": "https://vocenya.com/mcp/docs" } } } ``` ### Claude and Claude Desktop Add a custom connector under Settings → Connectors with the URL `https://vocenya.com/mcp/docs`. No authentication is needed. ### ChatGPT Add a custom connector in ChatGPT's developer mode with the URL `https://vocenya.com/mcp/docs` and no authentication. ## Without MCP Every docs page is also plain Markdown: add `.md` to its URL, or use the **Copy for LLM** menu on the page. [llms.txt](https://vocenya.com/docs/llms.txt) lists every page, and [llms-full.txt](https://vocenya.com/docs/llms-full.txt) has the whole docs in one file.