Webhooks

Receive completed analyses, failures, threshold breaches and alerts as signed HTTP deliveries: event types, payloads, headers, HMAC verification in Node, .NET and Python, retries and troubleshooting.

Webhooks push events from VoiceLens to a URL you control the moment they happen. Use them to route low-scoring calls into a QA queue, to store every analysis in your own warehouse, or to page someone when an alert fires. Every delivery is signed with HMAC-SHA256 so your endpoint can prove it came from VoiceLens.

Create an endpoint

Under Webhooks in the dashboard (Admin role):

  1. Add the URL and a name

    The URL must be https. Name the endpoint after the system that receives it.

  2. Choose events

    Subscribe to any combination of the five event types below.

  3. Optionally set a health-score threshold

    A value from 1 to 100. Completed calls scoring strictly below it, or containing at least one danger fault, additionally produce a threshold.breached event for this endpoint.

  4. Store the secret

    The signing secret (prefix whsec_) is shown once at creation and whenever you rotate it. Listings show only a hint.

  5. Send a test

    Send test event delivers a webhook.test payload so you can verify the signature before real traffic arrives.

The dashboard also lists recent deliveries with status codes and lets you redeliver any of them.

Event types

TypeWhen
conversation.completedA call finished transcription and analysis.
conversation.failedProcessing failed; data.conversation.processingError says why.
threshold.breachedA completed call scored below the endpoint's threshold or has a danger fault. Requires a threshold on the endpoint.
alert.triggeredAn alert rule crossed its threshold.
alert.resolvedThe rule's condition cleared (when recovery notifications are enabled).
webhook.testSent only by the test button.

Payload

Every delivery is a JSON envelope. Fields that do not apply are omitted rather than null.

{
  "id": "evt_01J8…",
  "type": "conversation.completed",
  "createdAt": "2026-09-16T09:41:12Z",
  "workspaceId": "b3c1…",
  "data": {
    "conversation": { "…": "see below" },
    "threshold": { "…": "threshold.breached only" },
    "alert": { "…": "alert.* only" },
    "message": "webhook.test only"
  }
}

data.conversation

Present on conversation.completed, conversation.failed and threshold.breached.

FieldMeaning
id, title, status, source, urlIdentity and a link to the call in the dashboard.
channel { id, name }The channel.
externalCallId, customYour identifiers and custom metadata from ingest, returned untouched.
agent { id, name, type }Agent identity from ingest metadata.
healthScore0 to 100; absent on failed calls.
durationSecCall length.
faults { total, danger, warning, info }Open fault counts.
completionRate, coverageRateSlot and question rates.
outcome, resolvedCaseLabelCall outcome and the rubric case branch.
evaluationTemplateVersionIdThe rubric version used.
startedAt, completedAt, createdAtTimestamps.
processingErrorOn failures.
isTestCalltrue for test-call recordings.

data.threshold

{ "healthScoreBelow": 70, "healthScore": 58, "dangerFaults": 1 }: the endpoint's threshold, the call's score and its danger-fault count.

data.alert

ruleId, eventId, name, metric, comparison, threshold, observedValue, sampleSize, window { kind, size }, optional channel { id, name }, triggeredAt, resolvedAt (on alert.resolved), sampleCalls[] (a few conversation summaries from the window) and url.

Headers

HeaderValue
X-VoiceLens-Signaturet=<unix seconds>,v1=<hex>
X-VoiceLens-EventThe event type.
X-VoiceLens-Event-IdThe envelope id. Stable across retries: use it for idempotency.
X-VoiceLens-DeliveryUnique per delivery attempt.
X-VoiceLens-TimestampThe same t as in the signature.
Content-Typeapplication/json
User-AgentVoiceLens-Webhooks/1.0

Verify the signature

The signature is computed over the timestamp and the raw request body:

v1 = hex( HMAC-SHA256( secret, "<t>" + "." + raw_body ) )

Compare with a constant-time comparison, and reject deliveries whose t is older than your tolerance (5 minutes is a sensible default) to defeat replays. Read the body as bytes before any JSON parsing; re-serialised JSON will not match.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyVoiceLens(rawBody, signatureHeader, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((kv) => kv.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;

  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1 ?? "", "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express: keep the raw body
app.post("/voicelens", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyVoiceLens(req.body.toString("utf8"), req.header("X-VoiceLens-Signature"), process.env.WHSEC)) {
    return res.status(400).send("bad signature");
  }
  const event = JSON.parse(req.body);
  // handle event.type …
  res.sendStatus(204);
});

Delivery, retries and disabling

  • A delivery succeeds on any 2xx response within 10 seconds. Respond quickly and process asynchronously.
  • Failures are retried up to 8 attempts with growing delays: 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours.
  • After 30 consecutive failures the endpoint is disabled and shown as such in the dashboard. Fix the receiver, then re-enable it.
  • Retries reuse the event id and a new X-VoiceLens-Delivery. Deduplicate on the event id.
  • Events are delivered at least once and not necessarily in order; use createdAt if ordering matters.

Rotate the secret

Rotate secret on an endpoint generates a new whsec_ value, shown once. Deliveries are signed with the new secret immediately, so update the receiver first if you cannot tolerate a few failures, or accept both secrets during the switch.

Troubleshooting

SymptomLikely cause
Signature never matchesThe body was parsed and re-serialised before hashing, or the wrong secret is in use after a rotation. Hash the raw bytes.
Deliveries time outThe receiver does work before responding. Return 204 first, process later.
Duplicate eventsNormal after a retry. Store processed event ids.
No threshold.breached eventsThe endpoint has no health-score threshold set, or calls score above it and have no danger faults.
Endpoint disabled30 consecutive failures. Check the delivery log for the response codes.