# Webhooks 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. MiniApp vs tenant webhooks This guide covers **outbound** webhooks (Karzoun notifies your server). For **inbound** provider webhooks configured inside MiniApp JSON, see [MiniApps webhooks](/miniapps/guides/webhooks). Where to manage webhooks Create and inspect subscriptions in **Developer → Webhooks** (`/developer/webhooks`) or via GraphQL mutations. Search the [GraphQL API reference](/developers/apis/public-api) for `Webhook`, `webhooksAdd`, and related operations. ## How delivery works When an event occurs, Karzoun: 1. Finds every **active** webhook subscribed to that `type` + `action` pair 2. Builds a versioned JSON envelope (see [Payload format](#payload-format)) 3. Signs the raw body and attaches auth headers 4. POSTs to your URL with up to **3 retry attempts** on failure 5. 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. ## Security 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](#rotate-signing-secret) grace period (24 hours), Karzoun also sends `X-Karzoun-Signature-256-Previous` signed with the previous secret so you can roll over without downtime. Reject unauthenticated requests Always verify the token or signature before processing a payload. If neither matches, respond with `401` and discard the body. ### Verify the HMAC signature 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** ```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** ```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); ``` ### Verify the token 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 a webhook 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**) | Discover available events Query `webhooksGetActions` to list every event your workspace can subscribe to. The catalog below reflects the current platform modules. **GraphQL** ```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** ```bash 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 } }" }' ``` Register a webhook in Playground **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. ### Other management operations | 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 | ## Payload format Every delivery uses the same envelope shape (`version` is currently **`1`**): ```json { "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: `.` (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": "", "object": { "_id": "..." } }`. Connectivity pings use `event: "system.ping"` with a short test message in `data`. ## Retries, timeouts, and circuit breaker | 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 | Respond quickly Return `2xx` as soon as you have accepted the payload. Offload heavy work to a background queue — slow handlers risk timeouts and retries. Idempotent handlers 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. ## Event catalog Events are identified by a **`type`** (module + resource) and **`action`** (what happened). In the delivered payload they appear as `event: "."`. Query `webhooksGetActions` for the live list. Subscriptions below are grouped by platform module. ### Customers (`core:customer`) | 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 | ### Conversations & inbox (`inbox:*`) | 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 | ### WhatsApp (`whatsapp:*`) | Event | `action` | Description | | --- | --- | --- | | `whatsapp:order.received` | `received` | Order received via WhatsApp commerce | | `whatsapp:flow.completed` | `completed` | WhatsApp Flow finished by the customer | ### Tasks (`tasks:*`) | 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 | ### Meetings (`meetings:meeting`) | 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 | ### Timeclock (`timeclock:*`) | 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 | ### System | Event | `action` | Description | | --- | --- | --- | | `system.ping` | `ping` | Connectivity test sent by `webhooksPing` or on webhook creation | ## Troubleshooting ### 1. Confirm Karzoun is sending events - Open **Developer → Webhooks → Delivery logs** for the subscription - Run `webhooksPing` and check for a `system.ping` delivery with status `success` - Temporarily point the URL to [webhook.site](https://webhook.site) to inspect raw requests ### 2. Check your endpoint | 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 | ### 3. Validate URL requirements - 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 ## Next steps - [GraphQL API Reference](/developers/apis/public-api) — search for `Webhook`, `WebhookDeliveryLog` - [Authentication](/developers/getting-started/authentication) — app tokens for GraphQL access - [Errors](/developers/guides/errors) — interpreting GraphQL and HTTP error responses List delivery logs in Playground