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.