Webhooks and signatures
Subscribe to derived events and verify Kite deliveries safely.
Create one endpoint per project and environment, subscribe to explicit event names, and use *
only when the receiver intentionally handles every derived event.
{
"environment": "live",
"url": "https://cs.example.com/webhooks/kite",
"events": ["customer.activated", "customer.churn_risk.detected"],
"secret": "replace-with-at-least-16-characters"
}Webhooks primarily deliver derived timeline events emitted by lifecycle, journey, segment, and rule evaluation. Do not assume every raw ingestion event produces a webhook.
Delivery contract
Kite-Signature: t=2026-07-19T12:00:00.000Z,v1=4f...
Kite-Event-Id: whe_timeline-id
Content-Type: application/json{
"id": "whe_timeline-id",
"event": "customer.activated",
"timestamp": "2026-07-19T11:59:59.000Z",
"project": "acme",
"environment": "live",
"data": { "customer_id": "account_42" }
}The signature is lowercase hexadecimal HMAC-SHA256 over the exact UTF-8 bytes:
<t value>.<raw request body>Verify the raw body before JSON parsing, compare signatures in constant time, and reject a
delivery timestamp outside your replay window. The t value is the delivery claim timestamp,
not the payload event timestamp.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyKite(rawBody: Buffer, header: string, secret: string) {
const match = /^t=([^,]+),v1=([a-f0-9]{64})$/.exec(header);
if (!match) return false;
const [, timestamp, suppliedHex] = match;
const claimedAt = Date.parse(timestamp);
if (!Number.isFinite(claimedAt) || Math.abs(Date.now() - claimedAt) > 5 * 60_000) return false;
const expected = createHmac('sha256', secret)
.update(timestamp)
.update('.')
.update(rawBody)
.digest();
const supplied = Buffer.from(suppliedHex, 'hex');
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}Retries and idempotency
Any 2xx response succeeds. Kite makes up to four attempts: immediately, then approximately 30
seconds, 5 minutes, and 30 minutes later. The default request timeout is 10 seconds.
Delivery is at-least-once. In a transaction, insert Kite-Event-Id into a table with a unique
constraint, apply the business side effect, and commit before returning 2xx. Return 2xx for a
previously processed ID. Return a non-2xx status only for a transient failure that should retry.
Keep endpoint work short and enqueue downstream processing locally. Rotate secrets by creating a new webhook, accepting both endpoints during the transition, then removing the old subscription. Dashboard-generated secrets are displayed once; API clients supply the secret during creation.