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.
Add an endpoint
Add endpoints in the portal under Developers, or with the API using a key with webhooks:manage:
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:readforlead.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:
{
"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, 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=<unix time>,v1=<hex signature>. 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:
v1 = hex(HMAC_SHA256(secret, "<t>.<raw body>"))To verify:
- Split the header on
,and readtandv1. - Reject the delivery if
tis more than 300 seconds (5 minutes) from your clock. This stops replays. - Compute the HMAC of
t + "." + raw bodywith your secret and compare it tov1in constant time.
Use the raw body exactly as received. Parsing the JSON and serialising it again changes the bytes and breaks the signature.
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);
});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:
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.