VoxiQa ← Settings → Webhooks
Developer Docs

VoxiQa Webhooks — event catalog & integration guide

VoxiQa delivers outbound webhooks so you can wire calls and bookings into n8n, Make, Zapier, GoHighLevel, HubSpot, or any HTTP endpoint. One signed JSON webhook works with all of them — no vendor-specific app required.

Setup (self-service)

Cabinet → Settings → Webhooks: 1. Add an endpoint (must be https, public host). 2. Pick the events to receive (or leave empty = all events). 3. Copy the signing secret — shown once. Rotate any time (old secret stops working). 4. Use Send test to confirm your receiver is reachable.

Delivery is asynchronous and durable: retried on 5xx/network errors; an endpoint that fails 10 times in a row is auto-quarantined (re-enable it in the cabinet after fixing). Every attempt is logged (status, error) under the endpoint's delivery history.

Request shape

POST your URL, Content-Type: application/json. Body envelope:

{
  "version": "1",
  "event": "BOOKING_CREATED",
  "ts": "2026-06-03T09:15:00.000000+00:00",
  "client_id": "<your tenant uuid>",
  "call_id": "call_abc123",
  "payload": { /* event-specific, see catalog */ }
}

Headers:

Header Meaning
X-VoxiQa-Signature sha256=<hex> — HMAC over "{timestamp}.{raw_body}"
X-VoxiQa-Timestamp unix seconds; reject if too old (replay defense)
X-VoxiQa-Event the event name (also in the body)
X-VoxiQa-Delivery-Id unique per attempt (idempotency / dedup)

Verifying the signature

Compute HMAC-SHA256 of "{X-VoxiQa-Timestamp}.{raw_request_body}" with your secret; compare (constant-time) to the hex in X-VoxiQa-Signature. Reject if the timestamp is more than ~5 minutes old.

Python:

import hashlib, hmac, time
def verify(secret, body_bytes, sig_header, ts_header, tolerance=300):
    if abs(time.time() - int(ts_header)) > tolerance:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode(), ts_header.encode() + b"." + body_bytes, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sig_header)

Node:

const crypto = require("crypto");
function verify(secret, rawBody, sig, ts, tolerance = 300) {
  if (Math.abs(Date.now() / 1000 - Number(ts)) > tolerance) return false;
  const expected = "sha256=" + crypto.createHmac("sha256", secret)
    .update(ts + "." + rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

Event catalog (tenant-facing)

payload fields are additive within version: "1" — new fields may appear over time; don't fail on unknown keys. call_id (top-level) ties events from the same call together; fetch fuller details via the API when needed.

Event When payload
CALL_ENDED a call finished (fired post-call, so outcome+summary are final) caller_phone, outcome, summary, duration_seconds, has_recording
BOOKING_CREATED appointment booked (incl. internal/manual catalog) booking_id, service, is_upsell, provider_booking_id, caller_phone
BOOKING_CANCELLED appointment cancelled booking_id, provider_booking_id, caller_phone
BOOKING_RESCHEDULED appointment moved booking_id, service, moved_in_place, caller_phone
UPSELL_ACCEPTED caller accepted an add-on service, caller_phone
ESCALATION_TRIGGERED call handed to a human reason (+ context)
LEAD_CAPTURED caller wanted to book but the agent couldn't complete it lead_id, caller_phone, caller_name, intent_type, requested_service, requested_datetime, reason
UNANSWERED_QUESTION agent couldn't answer from the knowledge base question

outcome (on CALL_ENDED) is one of: booked, cancelled, rescheduled, escalated, ticket, handled (consultation), no_intent, failed.

caller_phone is the caller's number — use it as the SMS/CRM key. Works even with no calendar/CRM connected (consultant + internal-catalog mode): events fire on what the agent did, not on any integration.

Internal QA signals (frustration/fallback/call-started/upsell-offered/declined) are not delivered to tenant webhooks.

TEST_PING is sent only by the cabinet's "Send test" button: payload = { "message": "...", "test": true }.

Payload fields are additive within version: "1" — new keys may appear; don't fail on unknown keys.

Recipes (no-code)

SMS confirmation to the caller (we don't send SMS — you do, via your provider): BOOKING_CREATED → n8n/Make/Zapier → Twilio/MessageBird "Send SMS" to payload.caller_phone (e.g. "You're booked for {service}. See you soon!").

Push every call into your CRM (works in consultant mode): CALL_ENDED → HubSpot/GoHighLevel "Create/Update Contact" keyed on payload.caller_phone; log payload.summary + payload.outcome as a note.

Follow up on missed bookings: LEAD_CAPTURED → create a task / send the caller an SMS using payload.caller_phone + payload.requested_service.

Improve your knowledge base: UNANSWERED_QUESTION → append payload.question to a sheet / Slack so you can add the answer or call the customer back.

voxiqa.com · Contact · Set up webhooks