Webhooks
Voxy delivers events to your endpoints as signed HTTP POSTs. Delivery is at-least-once — your handler must be idempotent (see Dedupe with event_id). Configure subscriptions with the webhooks:write scope.
Available events
The full event catalogue, sourced from the OpenAPI spec at build time. New events appear automatically when the API adds them:
call.completedcall.summary_readylead.createdlead.updatedcampaign.completedknowledge.ingestedHMAC verification
Every delivery carries an X-Voxy-Signature header — a hex-encoded HMAC-SHA256 of the raw request body, keyed by the subscription's signing secret. Verify it before trusting the payload.
import { createHmac, timingSafeEqual } from 'node:crypto';
/**
* Verify the X-Voxy-Signature header on an incoming webhook delivery.
* Returns true on a valid signature, false otherwise. ALWAYS read the raw
* request body — JSON.parse + re-stringify will produce a different hash.
*/
export function verifyVoxySignature({
rawBody,
signatureHeader,
secret,
}: {
rawBody: Buffer;
signatureHeader: string | undefined;
secret: string;
}): boolean {
if (!signatureHeader) return false;
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const received = Buffer.from(signatureHeader, 'utf8');
const want = Buffer.from(expected, 'utf8');
if (received.length !== want.length) return false;
return timingSafeEqual(received, want);
}
// Express handler — note the raw-body middleware.
app.post(
'/webhooks/voxy',
express.raw({ type: 'application/json' }),
(req, res) => {
const ok = verifyVoxySignature({
rawBody: req.body,
signatureHeader: req.header('x-voxy-signature'),
secret: process.env.VOXY_WEBHOOK_SECRET!,
});
if (!ok) return res.status(401).send('invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
handleVoxyEvent(event);
res.status(200).send('ok');
},
);Compare with timingSafeEqual — a naive === check leaks timing information.
Retry schedule
Voxy retries on any non-2xx response (and on connection errors) with exponential backoff. Six attempts span a little over seven hours:
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 30 s |
| 3 | 2 min |
| 4 | 10 min |
| 5 | 1 h |
| 6 | 6 h (final) |
After the 6th failed attempt the delivery moves to the workspace dead-letter queue (Settings → Webhooks → Deliveries) where you can inspect the payload and replay manually. To rotate signing secrets safely, mint a new subscription, deploy the new verifier, then delete the old subscription.
Dedupe with event_id
Every payload includes a stable event_id (ULID). Store processed ids in your own cache and short-circuit duplicates — this turns at-least-once delivery into exactly-once processing.
const SEEN = new Map<string, number>(); // use a shared/persistent store in production
export function handleVoxyEvent(event: { event_id: string; type: string; data: unknown }) {
if (SEEN.has(event.event_id)) return; // already processed; ack 200
SEEN.set(event.event_id, Date.now());
// …business logic…
}