Karzoun webhooks deliver real-time HTTP notifications when something changes in your workspace. Register an HTTPS endpoint, pick the events you care about, and Karzoun POSTs a structured JSON payload whenever they fire — for example when a conversation is assigned, a customer is updated, or a WhatsApp flow completes.
Use webhooks to sync CRM data, trigger automations in your stack, notify external systems, or feed event streams into your own analytics pipeline.
This guide covers outbound webhooks (Karzoun notifies your server). For inbound provider webhooks configured inside MiniApp JSON, see MiniApps webhooks.
Create and inspect subscriptions in Developer → Webhooks (/developer/webhooks) or via GraphQL mutations. Search the GraphQL API reference for Webhook, webhooksAdd, and related operations.
When an event occurs, Karzoun:
- Finds every active webhook subscribed to that
type+actionpair - Builds a versioned JSON envelope (see Payload format)
- Signs the raw body and attaches auth headers
- POSTs to your URL with up to 3 retry attempts on failure
- Records every attempt in delivery logs for debugging
Your endpoint should return any 2xx status within the connection timeout. Non-2xx responses and network errors trigger automatic retries with exponential backoff.
Karzoun secures webhook traffic with two independent mechanisms on every delivery. Use one or both to confirm the request genuinely came from Karzoun.
| Mechanism | Header | Purpose |
|---|---|---|
| Token | Karzoun-token | Static bearer token generated when the webhook is created |
| HMAC signature | X-Karzoun-Signature-256 | sha256= + HMAC-SHA256 of the raw request body using your webhook secret |
During a secret rotation grace period (24 hours), Karzoun also sends X-Karzoun-Signature-256-Previous signed with the previous secret so you can roll over without downtime.
Always verify the token or signature before processing a payload. If neither matches, respond with 401 and discard the body.
The signature header value is prefixed with sha256=. Strip that prefix, then compare the remainder to an HMAC you compute over the exact raw body bytes (not a re-serialized JSON object).
Node.js
const crypto = require('crypto');
function verifyKarzounWebhook(req, secret) {
const header = req.headers['x-karzoun-signature-256'];
if (!header || !header.startsWith('sha256=')) return false;
const expected = header.slice('sha256='.length);
const computed = crypto
.createHmac('sha256', secret)
.update(req.rawBody, 'utf8') // use the raw body string/buffer
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(computed, 'hex'),
);
}PHP
$secret = getenv('WEBHOOK_SECRET');
$header = $_SERVER['HTTP_X_KARZOUN_SIGNATURE_256'] ?? '';
$body = file_get_contents('php://input');
if (!str_starts_with($header, 'sha256=')) {
http_response_code(401);
exit;
}
$expected = substr($header, 7);
$computed = hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $computed)) {
http_response_code(401);
exit;
}
http_response_code(200);Compare the Karzoun-token header to the token returned when you created the webhook. This is a simple shared-secret check — pair it with HMAC verification for defense in depth.
Register subscriptions with webhooksAdd. Karzoun generates a unique token and HMAC secret, validates your URL (HTTPS + SSRF checks), and sends an automatic connectivity ping.
| Parameter | Type | Description |
|---|---|---|
url | string | Required. HTTPS endpoint that receives POST requests |
name | string | Human-readable label |
description | string | What this webhook is used for |
actions | array | Event subscriptions — each entry has type, action, and optional label |
retryPolicy.maxAttempts | int | Max delivery attempts (default: 3) |
retryPolicy.backoffMs | int[] | Delay between retries in ms (default: 5000, 30000, 120000) |
rateLimitPerMinute | int | Max deliveries per minute per webhook (default: 100) |
Query webhooksGetActions to list every event your workspace can subscribe to. The catalog below reflects the current platform modules.
GraphQL
mutation {
webhooksAdd(
url: "https://api.example.com/karzoun/events"
name: "CRM + inbox sync"
description: "Customer and conversation updates"
actions: [
{ type: "core:customer", action: "create" }
{ type: "core:customer", action: "update" }
{ type: "inbox:conversation", action: "create" }
{ type: "inbox:conversation", action: "assign" }
{ type: "whatsapp:flow", action: "completed" }
]
retryPolicy: {
maxAttempts: 3
backoffMs: [5000, 30000, 120000]
}
) {
_id
url
token
secret
actions { type action label }
retryPolicy { maxAttempts backoffMs }
}
}cURL
curl -X POST 'https://YOUR_SUBDOMAIN.api.karzoun.chat/graphql' \
-H 'Content-Type: application/json' \
-H 'x-app-token: YOUR_APP_TOKEN' \
-d '{
"query": "mutation { webhooksAdd(url: \"https://api.example.com/karzoun/events\", name: \"My hook\", actions: [{ type: \"core:customer\", action: \"update\" }]) { _id token secret } }"
}'Store credentials immediately. The token and secret are returned on creation. Use webhooksRotateSecret to roll the HMAC secret; the previous secret remains valid for 24 hours.
| Mutation | Description |
|---|---|
webhooksEdit | Update URL, name, actions, retry policy, or rate limit |
webhooksRemove | Delete a webhook and its delivery logs |
webhooksToggle | Pause or resume deliveries without deleting config |
webhooksRotateSecret | Generate a new HMAC secret (24h grace on the old one) |
webhooksPing | Send a test system.ping event to verify connectivity |
webhooksRetryDelivery | Manually retry a failed delivery log entry |
webhooksReset | Reset circuit breaker and error counters after fixing your endpoint |
webhookDeliveryLogs | Inspect request/response details for debugging |
Every delivery uses the same envelope shape (version is currently 1):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"version": "1",
"timestamp": "2026-06-23T14:30:00.000Z",
"event": "inbox:conversation.create",
"data": {
"_id": "abc123",
"status": "open",
"customerId": "cust_456"
},
"metadata": {
"webhookId": "wh_789",
"deliveryId": "del_012",
"attemptNumber": 1,
"description": "Customer has started a new conversation",
"resourceUrl": "https://YOUR_SUBDOMAIN.karzoun.chat/inbox?conversation=abc123"
}
}| Field | Description |
|---|---|
id | Unique event ID — use as an idempotency key to deduplicate retries |
event | Fully-qualified name: <type>.<action> (e.g. core:customer.update) |
data | Native JSON object for the affected resource — never double-stringified |
metadata.attemptNumber | Which delivery attempt this is (1 = first try) |
metadata.resourceUrl | Deep link to the resource in the Karzoun UI |
For delete events, data is normalized to { "type": "<type>", "object": { "_id": "..." } }.
Connectivity pings use event: "system.ping" with a short test message in data.
| Setting | Default | Behavior |
|---|---|---|
| HTTP timeout | 10 seconds | Karzoun waits up to 10s for your server to respond |
| Max attempts | 3 | Failed deliveries retry with backoff: 5s → 30s → 2min |
| Circuit breaker | 5 failures | After 5 consecutive failures, deliveries pause until cooldown (5 min) or you call webhooksReset |
| Rate limit | 100/min | Per-webhook cap; excess events are logged as skipped |
Return 2xx as soon as you have accepted the payload. Offload heavy work to a background queue — slow handlers risk timeouts and retries.
The same logical event may arrive more than once (id stays the same across retries; metadata.deliveryId changes per attempt). Design handlers to tolerate duplicates.
Events are identified by a type (module + resource) and action (what happened). In the delivered payload they appear as event: "<type>.<action>".
Query webhooksGetActions for the live list. Subscriptions below are grouped by platform module.
| Event | action | Description |
|---|---|---|
core:customer.create | create | A customer record was created |
core:customer.update | update | A customer record was updated |
core:customer.delete | delete | A customer record was deleted |
core:customer.merge | merge | Two customer records were merged |
| Event | action | Description |
|---|---|---|
inbox:conversation.create | create | A new conversation started |
inbox:conversation.status | status | Conversation status changed (e.g. open → closed) |
inbox:conversation.assign | assign | Conversation assigned to an agent |
inbox:conversation.unassign | unassign | Conversation unassigned |
inbox:conversation.update | update | Conversation metadata updated |
inbox:feedback.create | create | Customer submitted feedback |
inbox:summary.create | create | AI conversation summary generated |
inbox:popupSubmitted.create | create | Customer submitted a popup form |
| Event | action | Description |
|---|---|---|
whatsapp:order.received | received | Order received via WhatsApp commerce |
whatsapp:flow.completed | completed | WhatsApp Flow finished by the customer |
| Event | action | Description |
|---|---|---|
tasks:task.create | create | Task created |
tasks:task.update | update | Task updated |
tasks:task.delete | delete | Task deleted |
tasks:task.assign | assign | Task assigned |
tasks:task.stage_change | stage_change | Task moved to a different stage |
tasks:task.priority_change | priority_change | Task priority changed |
tasks:task.due_date | due_date | Task due date changed |
tasks:checklist.item_checked | item_checked | Checklist item marked complete |
| Event | action | Description |
|---|---|---|
meetings:meeting.create | create | Meeting scheduled |
meetings:meeting.update | update | Meeting details changed |
meetings:meeting.cancel | cancel | Meeting cancelled |
meetings:meeting.start | start | Meeting started |
| Event | action | Description |
|---|---|---|
timeclock:shift.clock_in | clock_in | Employee clocked in |
timeclock:shift.clock_out | clock_out | Employee clocked out |
timeclock:absence.requested | requested | Absence / time-off requested |
timeclock:absence.approved | approved | Absence request approved |
timeclock:absence.rejected | rejected | Absence request rejected |
timeclock:schedule.submitted | submitted | Schedule submitted for approval |
timeclock:schedule.approved | approved | Schedule approved |
timeclock:schedule.rejected | rejected | Schedule rejected |
| Event | action | Description |
|---|---|---|
system.ping | ping | Connectivity test sent by webhooksPing or on webhook creation |
- Open Developer → Webhooks → Delivery logs for the subscription
- Run
webhooksPingand check for asystem.pingdelivery with statussuccess - Temporarily point the URL to webhook.site to inspect raw requests
| Symptom | Likely cause |
|---|---|
| No requests at all | Webhook paused (isActive: false), circuit open, or wrong event subscription |
401 on your side | Token/signature verification failing — compare raw body, not parsed JSON |
| Timeouts | Handler too slow; Karzoun aborts after 10 seconds |
circuit-open status | Five consecutive failures — fix the endpoint, then webhooksReset |
| Skipped deliveries | Rate limit exceeded for that webhook |
- Must be HTTPS
- Must resolve to a public IP (private ranges and localhost are blocked for SSRF protection)
- Must return 2xx for Karzoun to consider delivery successful
- GraphQL API Reference — search for
Webhook,WebhookDeliveryLog - Authentication — app tokens for GraphQL access
- Errors — interpreting GraphQL and HTTP error responses