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:
| Event | Trigger |
|---|---|
entity.created | A new canonical entity is created. |
entity.updated | An entity's golden record fields change. |
entity.merged | Two or more entities are merged into one. |
entity.split | An entity is split into separate entities. |
entity.deleted | An entity is removed. |
reconciliation.completed | A batch reconciliation job finishes successfully. |
reconciliation.failed | A batch reconciliation job fails. |
source.ingested | A 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.
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"
}'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 onceimport { 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 onceclient := 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 onceResponse (201 Created):
{
"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.
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", 200const 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:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Kanoniv-Signature | sha256=<hex-encoded HMAC-SHA256> |
X-Kanoniv-Event | The event type (e.g., entity.merged). |
X-Kanoniv-Delivery | Unique 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:
| Attempt | Delay |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 60 seconds |
| 3rd retry | 5 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.
curl https://api.kanoniv.com/v1/outbound-webhooks \
-H "X-API-Key: kn_..."webhooks = client.webhooks.list()
for wh in webhooks:
print(f"{wh['id']}: {wh['url']} ({wh['secret_last4']})")const webhooks = await client.webhooks.list();
for (const wh of webhooks) {
console.log(`${wh.id}: ${wh.url}`);
}webhooks, err := client.Webhooks.List(ctx)
for _, wh := range webhooks {
fmt.Printf("%s: %s\n", wh.ID, wh.URL)
}Response (200 OK):
[
{
"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
curl https://api.kanoniv.com/v1/outbound-webhooks/{id} \
-H "X-API-Key: kn_..."webhook = client.webhooks.get("wh-abc123")const webhook = await client.webhooks.get('wh-abc123');webhook, err := client.Webhooks.Get(ctx, "wh-abc123")Delete a webhook
Deactivates the webhook. Pending deliveries will not be sent.
curl -X DELETE https://api.kanoniv.com/v1/outbound-webhooks/{id} \
-H "X-API-Key: kn_..."client.webhooks.delete("wh-abc123")await client.webhooks.delete('wh-abc123');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.
curl -X POST https://api.kanoniv.com/v1/outbound-webhooks/{id}/test \
-H "X-API-Key: kn_..."result = client.webhooks.test("wh-abc123")
print(f"Success: {result['success']}, HTTP {result['http_status']}")const result = await client.webhooks.test('wh-abc123');
console.log(`Success: ${result.success}, HTTP ${result.http_status}`);result, err := client.Webhooks.Test(ctx, "wh-abc123")
fmt.Printf("Success: %v\n", result["success"])Response:
{
"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.
curl "https://api.kanoniv.com/v1/outbound-webhooks/{id}/deliveries?limit=20" \
-H "X-API-Key: kn_..."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']})")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})`);
}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:
[
{
"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
| Parameter | Value |
|---|---|
| Max webhooks per tenant | 5 |
| URL protocol | HTTPS only |
| URL max length | 2,048 characters |
| Delivery timeout | 10 seconds |
| Max retry attempts | 3 |
| Delivery retention | 7 days |
| Description max length | 500 characters |
Best practices
Always verify signatures. Never process a webhook payload without validating the
X-Kanoniv-Signatureheader. This protects against spoofed requests.Return 200 quickly. Process events asynchronously. If your handler takes too long, the delivery will time out and be retried, causing duplicate processing.
Use the delivery ID for deduplication. The
X-Kanoniv-Deliveryheader is unique per delivery. Store processed delivery IDs to avoid handling the same event twice on retries.Monitor your delivery log. Check the delivery history endpoint periodically to catch persistent failures before they accumulate.
Use test deliveries during setup. Before subscribing to real events, use the test endpoint to verify your signature verification and endpoint connectivity.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v1/outbound-webhooks | Create a webhook |
GET | /v1/outbound-webhooks | List webhooks |
GET | /v1/outbound-webhooks/{id} | Get a webhook |
DELETE | /v1/outbound-webhooks/{id} | Delete a webhook |
POST | /v1/outbound-webhooks/{id}/test | Send test event |
GET | /v1/outbound-webhooks/{id}/deliveries | Delivery history |
