Skip to content

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:

Shell
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, 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:

Text
v1 = hex(HMAC_SHA256(secret, "<t>.<raw body>"))

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.

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:

Shell
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.