Webhooks
Voxy delivers signed JSON events to your URL on every action your workspace cares about — a call ending, a complaint being detected, a lead going hot. There are two channels you can configure:
- Notifications → Webhook channel (recommended). Manage at
/workspace/tools/notifications. Same signing model as below; you also get Email and WhatsApp channels driven by the same event catalog. - Legacy /workspace/tools/webhooks. Older per-workspace webhook surface — still supported, but new workspaces should use the Notifications page.
Event catalog
Group your subscriptions by what you actually want notified about. Every event ships the same envelope shape:
{
"event": "complaint.detected",
"emittedAt": "2026-05-21T09:14:00.123Z",
"workspace": { "id": "01HZ…", "name": "Acme Co." },
"payload": { /* event-specific shape — see below */ }
}Calls
call.ended— every call after the AI summary lands.call.inbound_answered,call.outbound_placed,call.missed,call.transferred_to_human.
Customer signals
complaint.detected— payload addsticket { id, severity, summary, detailUrl }.positive_review.captured,spam.detected.
Operations
callback.requested— payload addscallback { id, scheduledFor, reason }.booking.created/booking.confirmed/booking.cancelled— payload addsbooking { id, kind, title, scheduledAt, totalMinorUnits, detailUrl }.info_request.created— agent couldn't answer; payload addsinfoRequest { id, question, context, slaDueAt, detailUrl }.info_request.responded— operator answered; payload addsresponse+respondedBy.info_request.unreachable— 3 callback attempts failed.
Leads
lead.upgraded_to_hot,lead.downgraded_to_cold,lead.went_dead— payload carries the lead row + the previous score / temperature so you can render a diff.
Campaigns
campaign.started,campaign.paused,campaign.completed— payload adds stats (total, attempted, answered, completed).
Billing
quota.minutes_low,quota.exhausted— payload carries remaining minutes + a billing URL.
Delivery semantics
- At-least-once. Every delivery carries
x-voxy-delivery-id(ULID). De-dupe on it with a 24-hour lookback. - Timeout. 8 seconds per attempt — your consumer must accept and queue the work, not process it inline.
- Failure handling. 5 consecutive failures auto- pauses the channel; you'll see
status=pausedon the Notifications page with the last error message. Resume by editing the channel and saving.
Signing (HMAC-SHA256)
Configure a Signing secret on the Webhook channel. On every delivery Voxy computes:
signature = "sha256=" + hex(hmac_sha256(secret, raw_body))
header = "x-voxy-signature: " + signatureVerify before you trust the body. Reject anything where the recomputed digest doesn't match what arrived in the header. Use constant-time comparison.
Request headers
| Header | Meaning |
|---|---|
x-voxy-event | The event key (e.g. complaint.detected). |
x-voxy-delivery-id | ULID — unique per delivery attempt; use for idempotency. |
x-voxy-signature | sha256=<hex> when a signing secret is set. |
content-type | application/json. |
Receiving in Node (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.VOXY_WEBHOOK_SECRET!;
app.post('/voxy', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body as Buffer;
const sig = String(req.headers['x-voxy-signature'] ?? '');
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(rawBody)
.digest('hex');
// constant-time compare
if (
sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
) {
return res.status(401).end();
}
const event = JSON.parse(rawBody.toString());
console.log('voxy event', event.event, 'id=' + event.payload?.call?.id);
res.status(204).end();
});Testing
Use the test-send button on each channel (/workspace/tools/notifications) to fire a synthetic payload through the full pipeline. The payload matches the live schema for the chosen event so your verifier won't need special-case branches.

