# Webhooks This page explains how to receive real-time notifications from the Kanbert API via webhooks. You can register one or more webhook endpoints that will be called when specific events happen in your workspace. ## Overview - You register webhooks via the public API. - Each webhook belongs to your integration user and is scoped to your domain/workspace. - For every delivery we sign the JSON payload with an HMAC SHA-256 signature, using a per-subscription secret. - Your endpoint must respond with a 2xx HTTP status within a few seconds to be considered successful. ## Webhook Registration & Permissions All webhook management endpoints require the `integration` scope. You'll find a detailed description of the endpoints in the [API Reference](/api/webhook-management). Create a webhook via the endpoint to get started. Webhooks can be scoped by Project or Client. Specific webhooks for `task:*` and `comment:*` have to be scoped to a Project. ## Delivery, retries, timeouts, and debouncing - HTTP method: `POST` - Headers include `Content-Type: application/json` and a signature header (see below). - Timeout: if your endpoint doesn’t respond within ~3 seconds, the attempt is considered failed. - Retries: failed deliveries are retried automatically (default total tries: 3) with exponential backoff. - Success criteria: any `2xx` response code. ### Debouncing - To avoid noisy bursts when a resource is created/updated multiple times within a very short time window, webhook deliveries are debounced. - The payload's `data` contains the latest model state at the time of delivery. Special rules within the debounce window: - Create absorbs updates: if a resource is created and then updated quickly, a single webhook is sent with `event = …:create` and `data` reflecting the latest state after those updates. - Delete suppression for pending create: if a delete/force-delete occurs before the debounced delivery of a create, the pending delivery is cancelled and no webhook is sent at all. - Update → delete sequence: if the window starts with an update and a delete/force-delete happens before send, two webhooks are delivered in order: first `…:update` (latest data), then `…:delete`. ## Security: verifying signatures Every webhook request contains an HMAC SHA-256 signature of the JSON payload, computed with your webhook’s secret. - Header name: `Signature` - Algorithm: HMAC-SHA256 over the exact request body (raw JSON string) - Key: your `signature_secret_key` from creation Example verification (pseudo code): ``` expected = hex_hmac_sha256(raw_body, signature_secret_key) received = request.headers['Signature'] if !timingSafeEqual(expected, received): reject(401) ``` Notes: - Always validate the `Signature` before processing. - Use a timing-safe comparison to avoid timing attacks. - Only accept `application/json` and the expected fields. ## Payload structure Each delivery has this base shape. "data" always contains the full model data. "changes" and "previous" only contain changed attributes and are not always present. ```json { "event": "EVENT", "resource": { "type": "TYPE", "id": "ID" }, "triggered_by": "UserData", "data": "ModelData" } ``` Fields: - `event`: the event name. - `resource.type`: the resource type involved (e.g., `contact`, `company`). - `resource.id`: the obfuscated identifier of the resource in our system. - `triggered_by`: the user data that triggered the event. - `data`: Current model data. Example: contact created ```json { "event": "contact:create", "resource": { "type": "contact", "id": "X9abC3" }, "triggered_by": { "id": "yxZ21d", "email": "user@emai", "first_name": "Ada", "last_name": "Lovelace", "type": "user" }, "data": { "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" } } ``` ## Available events You can subscribe to one event per webhook - `contact:create` - `contact:update` - `contact:delete` - `company:create` - `company:update` - `company:delete` - `task:create` - `task:update` - `task:delete` - `comment:create` - `comment:update` - `comment:delete` This list reflects the current `WebhookEvent` enum used by the API. ## Best practices - Use a unique, secret URL for your webhook endpoint. - Restrict accepted methods to `POST` and validate `Content-Type: application/json`. - Verify the `Signature` header using your stored `signature_secret_key`. - Respond quickly (within a couple of seconds); you can enqueue heavy work and return `204 No Content` immediately. - Implement idempotency by de-duplicating events on your side if needed. - Log failed verifications and non-2xx responses for troubleshooting. ## Troubleshooting - 409 on create: a webhook with the same `url` and `event` already exists for your user. - Signature mismatches: compare against the exact raw body you received and the correct secret; watch out for whitespace transformations. - Long processing times: return early with a `2xx` and handle work asynchronously to avoid timeouts and retries.