Qanivo Event Hooks and External Consumers
Event Hooks deliver minimized Qanivo events from a Workspace to a subscriber. They are not Provider Webhooks and do not grant provider credentials. The subscriber is a non-human External Consumer with a scoped API identity.
Create a scoped External Consumer
External Consumer scope is derived from the authenticated administration context and stored identity. Do not send a caller-selected Workspace ID as authority. The generated API key is shown only according to the key-lifecycle contract; store it in a secret manager and rotate/revoke it through the API.
POST /api/external/v1/consumers HTTP/1.1
Authorization: Bearer <YOUR_ADMIN_SESSION>
Content-Type: application/json
Idempotency-Key: <YOUR_CONSUMER_OPERATION_ID>
{
"name": "billing-events-consumer",
"scopes": ["event_hooks:read", "event_hooks:deliver"]
}
The External Consumer is not a human role and cannot read provider secrets or bypass Workspace permissions. Treat its API key like a password: never put it in browser code, URLs, source control, webhook payloads, or logs.
Create a subscription
POST /api/external/v1/event-hooks/subscriptions HTTP/1.1
Authorization: Bearer <YOUR_EXTERNAL_CONSUMER_API_KEY>
Content-Type: application/json
Idempotency-Key: <YOUR_SUBSCRIPTION_OPERATION_ID>
{
"url": "https://hooks.example.invalid/qanivo",
"eventTypes": ["conversation.started", "message.queued"],
"allowUnsigned": false,
"secret": "<YOUR_SIGNING_SECRET>",
"isActive": true
}
The endpoint must satisfy the destination policy. Subscription creation, update, rotation, disablement, and deletion are auditable. The current implementation validates the catalog values shown above; lead lifecycle names belong to the Lead Delivery contract and are not interchangeable with Event Hook event names.
Receive a minimized event
An envelope is versioned, typed, bounded, redacted, and scoped. The exact raw request body must be verified before JSON parsing. For a signed subscription with a configured secret, the current Event Hook worker emits X-Qanivo-Webhook-Id, X-Qanivo-Webhook-Timestamp, X-Qanivo-Webhook-Event, and X-Qanivo-Webhook-Signature: v1=...; an unsigned subscription may omit these headers according to its configuration. Use the exact current framing in Signing and verification.
The following receiver is illustrative pseudocode: provide the timestamp-window and durable replay helpers in your server framework.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyEventHook(rawBody: Buffer, signature: string, timestamp: string, webhookId: string) {
const signingSecret = process.env.QANIVO_EVENT_HOOK_SECRET;
if (!signingSecret || !signature || !timestamp || !webhookId) throw new Error('missing verification context');
const expected = createHmac('sha256', signingSecret)
.update(`${timestamp}.${webhookId}.`)
.update(rawBody)
.digest('hex');
const actual = Buffer.from(signature.replace(/^v1=/, ''), 'hex');
const expectedBuffer = Buffer.from(expected, 'hex');
if (actual.length !== expectedBuffer.length || !timingSafeEqual(actual, expectedBuffer)) {
throw new Error('invalid signature');
}
if (!isFreshTimestamp(timestamp)) throw new Error('stale delivery');
return JSON.parse(rawBody.toString('utf8'));
}
The secret above is a server environment reference, not a value to copy into an application or document. The current Event Hook signature has no nonce header; use the webhook ID, timestamp window, and a durable body/signature idempotency record to reject replay. Reject invalid versions, unknown event types, oversized bodies, and mismatched Workspace/resource references before dispatching business work.
Delivery and disablement
Each delivery has an attempt identifier, event identifier, subscription, correlation ID, status, and safe outcome. A 2xx acknowledgement means the endpoint accepted the transport request; it does not mean the business operation succeeded. Non-success responses, timeouts, and network failures follow bounded retry policy. Repeated terminal failure may disable a subscription and emit an operational signal; inspect delivery attempts rather than assuming silent loss.