Skip to content

Webhooks

Webhooks push real-time notifications to your server when events happen in Kanoniv. Instead of polling the API, register an HTTPS endpoint and Kanoniv will POST event payloads to it as they occur.

Event types

Subscribe to any combination of these events:

EventTrigger
entity.createdA new canonical entity is created.
entity.updatedAn entity's golden record fields change.
entity.mergedTwo or more entities are merged into one.
entity.splitAn entity is split into separate entities.
entity.deletedAn entity is removed.
reconciliation.completedA batch reconciliation job finishes successfully.
reconciliation.failedA batch reconciliation job fails.
source.ingestedA data source ingestion completes.

Create a webhook

Register an HTTPS endpoint and choose which events to subscribe to. The secret is returned once at creation - store it securely.

bash
curl -X POST https://api.kanoniv.com/v1/outbound-webhooks \
  -H "X-API-Key: kn_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/kanoniv",
    "event_types": ["entity.merged", "reconciliation.completed"],
    "description": "Production merge notifications"
  }'
python
from kanoniv import Client

client = Client(api_key="kn_...")
webhook = client.webhooks.create(
    url="https://your-server.com/webhooks/kanoniv",
    event_types=["entity.merged", "reconciliation.completed"],
    description="Production merge notifications",
)
print(webhook["secret"])  # Store this - shown only once
typescript
import { KanonivClient } from '@kanoniv/sdk';

const client = new KanonivClient({ apiKey: 'kn_...' });
const webhook = await client.webhooks.create({
  url: 'https://your-server.com/webhooks/kanoniv',
  eventTypes: ['entity.merged', 'reconciliation.completed'],
  description: 'Production merge notifications',
});
console.log(webhook.signing_secret); // Store this - shown only once
go
client := kanoniv.NewClient(kanoniv.WithAPIKey("kn_..."))
webhook, err := client.Webhooks.Create(ctx, &kanoniv.CreateWebhookParams{
    URL:         "https://your-server.com/webhooks/kanoniv",
    EventTypes:  []string{"entity.merged", "reconciliation.completed"},
    Description: "Production merge notifications",
})
fmt.Println(*webhook.SigningSecret) // Store this - shown only once

Response (201 Created):

json
{
  "id": "a1b2c3d4-...",
  "url": "https://your-server.com/webhooks/kanoniv",
  "secret": "7f3a8b2c...64-char-hex-string",
  "event_types": ["entity.merged", "reconciliation.completed"],
  "active": true,
  "description": "Production merge notifications",
  "created_at": "2026-03-01T10:00:00Z",
  "updated_at": "2026-03-01T10:00:00Z",
  "note": "Store the secret securely - it will not be shown again."
}

Verify webhook signatures

Every webhook delivery includes an HMAC-SHA256 signature in the X-Kanoniv-Signature header. Always verify this signature before processing the payload.

python
import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# In your webhook handler:
from flask import request

@app.post("/webhooks/kanoniv")
def handle_webhook():
    payload = request.get_data()
    signature = request.headers.get("X-Kanoniv-Signature")

    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        return "Invalid signature", 401

    event = request.get_json()
    event_type = request.headers.get("X-Kanoniv-Event")

    if event_type == "entity.merged":
        handle_merge(event)
    elif event_type == "reconciliation.completed":
        handle_reconciliation(event)

    return "OK", 200
javascript
const crypto = require("crypto");

function verifySignature(payload, signature, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(payload).digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Express handler
app.post("/webhooks/kanoniv", (req, res) => {
  const payload = req.rawBody;
  const signature = req.headers["x-kanoniv-signature"];

  if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(payload);
  console.log(`Received ${req.headers["x-kanoniv-event"]}:`, event);
  res.sendStatus(200);
});

Delivery headers

Every webhook delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Kanoniv-Signaturesha256=<hex-encoded HMAC-SHA256>
X-Kanoniv-EventThe event type (e.g., entity.merged).
X-Kanoniv-DeliveryUnique delivery ID for deduplication.

Retries and failure handling

If your endpoint returns a non-2xx status code or the request times out (10 seconds), Kanoniv retries with exponential backoff:

AttemptDelay
1st retry10 seconds
2nd retry60 seconds
3rd retry5 minutes

After 3 failed attempts, the delivery is marked as permanently failed. You can view failed deliveries in the delivery log.

List webhooks

Returns all webhooks for your tenant. Secrets are masked to the last 4 characters.

bash
curl https://api.kanoniv.com/v1/outbound-webhooks \
  -H "X-API-Key: kn_..."
python
webhooks = client.webhooks.list()
for wh in webhooks:
    print(f"{wh['id']}: {wh['url']} ({wh['secret_last4']})")
typescript
const webhooks = await client.webhooks.list();
for (const wh of webhooks) {
  console.log(`${wh.id}: ${wh.url}`);
}
go
webhooks, err := client.Webhooks.List(ctx)
for _, wh := range webhooks {
    fmt.Printf("%s: %s\n", wh.ID, wh.URL)
}

Response (200 OK):

json
[
  {
    "id": "a1b2c3d4-...",
    "url": "https://your-server.com/webhooks/kanoniv",
    "secret_last4": "...8b2c",
    "event_types": ["entity.merged", "reconciliation.completed"],
    "active": true,
    "description": "Production merge notifications",
    "created_at": "2026-03-01T10:00:00Z",
    "updated_at": "2026-03-01T10:00:00Z"
  }
]

Get a webhook

bash
curl https://api.kanoniv.com/v1/outbound-webhooks/{id} \
  -H "X-API-Key: kn_..."
python
webhook = client.webhooks.get("wh-abc123")
typescript
const webhook = await client.webhooks.get('wh-abc123');
go
webhook, err := client.Webhooks.Get(ctx, "wh-abc123")

Delete a webhook

Deactivates the webhook. Pending deliveries will not be sent.

bash
curl -X DELETE https://api.kanoniv.com/v1/outbound-webhooks/{id} \
  -H "X-API-Key: kn_..."
python
client.webhooks.delete("wh-abc123")
typescript
await client.webhooks.delete('wh-abc123');
go
err := client.Webhooks.Delete(ctx, "wh-abc123")

Returns 204 No Content on success.

Test a webhook

Send a test event to verify your endpoint is reachable and signature verification works.

bash
curl -X POST https://api.kanoniv.com/v1/outbound-webhooks/{id}/test \
  -H "X-API-Key: kn_..."
python
result = client.webhooks.test("wh-abc123")
print(f"Success: {result['success']}, HTTP {result['http_status']}")
typescript
const result = await client.webhooks.test('wh-abc123');
console.log(`Success: ${result.success}, HTTP ${result.http_status}`);
go
result, err := client.Webhooks.Test(ctx, "wh-abc123")
fmt.Printf("Success: %v\n", result["success"])

Response:

json
{
  "success": true,
  "http_status": 200,
  "delivery_id": "e5f6a7b8-..."
}

The test event uses event type webhook.test and includes a sample payload. Your endpoint should return any 2xx status code.

View delivery history

List recent deliveries for a webhook to debug failures or verify delivery.

bash
curl "https://api.kanoniv.com/v1/outbound-webhooks/{id}/deliveries?limit=20" \
  -H "X-API-Key: kn_..."
python
deliveries = client.webhooks.deliveries("wh-abc123", limit=20)
for d in deliveries:
    print(f"{d['event_type']}: {d['status']} (attempt {d['attempts']}/{d['max_attempts']})")
typescript
const deliveries = await client.webhooks.deliveries('wh-abc123', { limit: 20 });
for (const d of deliveries) {
  console.log(`${d.event_type}: ${d.status} (attempt ${d.attempt})`);
}
go
deliveries, err := client.Webhooks.Deliveries(ctx, "wh-abc123", &kanoniv.DeliveryListParams{Limit: 20})
for _, d := range deliveries {
    fmt.Printf("%s: %s (attempt %d)\n", d.EventType, d.Status, d.Attempt)
}

Response:

json
[
  {
    "id": "d1e2f3a4-...",
    "webhook_id": "a1b2c3d4-...",
    "event_id": "b2c3d4e5-...",
    "event_type": "entity.merged",
    "status": "delivered",
    "http_status": 200,
    "attempts": 1,
    "max_attempts": 3,
    "last_error": null,
    "created_at": "2026-03-01T10:05:00Z",
    "delivered_at": "2026-03-01T10:05:01Z"
  },
  {
    "id": "f5a6b7c8-...",
    "webhook_id": "a1b2c3d4-...",
    "event_id": "c3d4e5f6-...",
    "event_type": "reconciliation.completed",
    "status": "failed",
    "http_status": 500,
    "attempts": 3,
    "max_attempts": 3,
    "last_error": "HTTP 500",
    "created_at": "2026-03-01T09:00:00Z",
    "delivered_at": null
  }
]

Delivery statuses: pending, delivered, failed.

Limits

ParameterValue
Max webhooks per tenant5
URL protocolHTTPS only
URL max length2,048 characters
Delivery timeout10 seconds
Max retry attempts3
Delivery retention7 days
Description max length500 characters

Best practices

  1. Always verify signatures. Never process a webhook payload without validating the X-Kanoniv-Signature header. This protects against spoofed requests.

  2. Return 200 quickly. Process events asynchronously. If your handler takes too long, the delivery will time out and be retried, causing duplicate processing.

  3. Use the delivery ID for deduplication. The X-Kanoniv-Delivery header is unique per delivery. Store processed delivery IDs to avoid handling the same event twice on retries.

  4. Monitor your delivery log. Check the delivery history endpoint periodically to catch persistent failures before they accumulate.

  5. Use test deliveries during setup. Before subscribing to real events, use the test endpoint to verify your signature verification and endpoint connectivity.

Endpoints

MethodPathDescription
POST/v1/outbound-webhooksCreate a webhook
GET/v1/outbound-webhooksList webhooks
GET/v1/outbound-webhooks/{id}Get a webhook
DELETE/v1/outbound-webhooks/{id}Delete a webhook
POST/v1/outbound-webhooks/{id}/testSend test event
GET/v1/outbound-webhooks/{id}/deliveriesDelivery history

The identity and delegation layer for AI agents.