Docs
Guides

Webhooks and signatures

Subscribe to derived events, verify signatures, and process at-least-once delivery safely.

Webhooks are scoped to a project and environment. You can create multiple subscriptions, subject to the project limit for your plan. Prefer explicit event names; use * only when the receiver handles every webhook-eligible named event.

Create a subscription

The dashboard generates a secret and displays it once. Administrative API clients provide their own secret of at least 16 characters:

{
  "environment": "live",
  "url": "https://cs.example.com/webhooks/kite",
  "events": ["customer.activated", "customer.churn_risk.detected"],
  "secret": "replace-with-at-least-16-characters"
}

Webhook endpoints must use HTTP(S), must not embed credentials, and must resolve only to public addresses. Live webhooks must use HTTPS. URLs can contain at most 2,048 characters, and each webhook can subscribe to between 1 and 50 event names. Redirects are not followed, so configure the final URL directly.

A new webhook receives only eligible events created after the subscription. It does not replay historical timeline events.

Know what is delivered

Webhooks deliver eligible named timeline events, including emissions from lifecycle, journey, segment, and rule evaluation. They do not deliver every state change or timeline record, and a normal raw product event does not automatically become a webhook event.

For example, this journey emission can match a subscription:

onComplete: { emit: 'customer.activated' }

* means every webhook-eligible named event, not every lifecycle, health, journey, or segment record.

Delivery contract

Each request includes:

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",
    "event": "customer.activated",
    "source": "journey"
  }
}
FieldMeaning
idStable webhook event ID, also sent as Kite-Event-Id.
eventName used by subscription filters.
timestampSource timeline event time, not delivery time.
projectProject slug.
environmenttest or live.
dataExtensible source data, always including customer_id.

Do not reject a delivery only because data contains a new field.

Verify the signature

Each v1 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 every v1 signature in constant time, and accept the request when any one matches. Reject a claim timestamp outside your replay window. The t value is the delivery attempt time, not the payload event timestamp.

During secret rotation, Kite-Signature contains one v1 value for the new secret and another for the previous secret. Supporting repeated v1 values lets you deploy the new secret without interrupting delivery.

verify-kite.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyKite(rawBody: Buffer, header: string, secret: string) {
  const parts = header.split(',');
  const timestampPart = parts.find((part) => part.startsWith('t='));
  const signatures = parts
    .filter((part) => part.startsWith('v1='))
    .map((part) => part.slice(3))
    .filter((value) => /^[a-f0-9]{64}$/.test(value));
  if (!timestampPart || signatures.length === 0) return false;

  const timestamp = timestampPart.slice(2);
  const claimedAt = Date.parse(timestamp);
  if (!Number.isFinite(claimedAt)) return false;
  if (Math.abs(Date.now() - claimedAt) > 5 * 60_000) return false;

  const expected = createHmac('sha256', secret)
    .update(timestamp)
    .update('.')
    .update(rawBody)
    .digest();

  return signatures.some((signature) => {
    const supplied = Buffer.from(signature, 'hex');
    return supplied.length === expected.length && timingSafeEqual(supplied, expected);
  });
}

Framework body parsers often consume or transform request bytes. Capture the raw body before calling JSON.parse.

Process idempotently

Delivery is at least once. Duplicates can occur after normal retries, ambiguous network outcomes, or recovery of an expired worker lease.

Process each request in one local transaction:

  1. Verify signature and timestamp.
  2. Insert Kite-Event-Id into a table with a unique constraint.
  3. If the ID already exists, return 2xx without repeating the effect.
  4. Apply the business side effect or enqueue durable local work.
  5. Commit, then return 2xx.

Do not use payload timestamp or customer ID as the deduplication key.

Retries

Any HTTP 2xx response succeeds. For retryable failures, Kite makes up to four automatic attempts: immediately, then approximately 30 seconds, 5 minutes, and 30 minutes later. The request timeout is 10 seconds.

Kite treats 400, 401, 403, 404, and 410 as permanent failures and does not retry them. Other non-2xx responses and network failures use the automatic schedule. For 429 and 503, a valid Retry-After response header overrides the next scheduled delay.

Automatic retries preserve Kite-Event-Id and the exact JSON payload, but each attempt uses a new t value and signature.

Keep endpoint work short. Queue slow processing durably and return only after the event is safely stored.

Test, monitor, and rotate

The dashboard supports test delivery and inspection of delivery attempts. A manual resend queues the stored payload for redelivery and is separate from the automatic four-attempt schedule.

Monitor:

  • non-2xx responses and timeouts;
  • duplicate event IDs;
  • processing lag from payload timestamp to completion;
  • signature failures and stale claim timestamps.

Rotate a secret from the webhook actions in the dashboard. The new secret is displayed once. For the configured overlap window (24 hours by default), Kite signs each request with both the new and previous secrets. Deploy the new secret to your receiver during that window; afterward, Kite stops including the previous signature automatically.

Do not rotate again until every receiver uses the current secret. A second rotation replaces the previous secret and ends its overlap early. The administrative API currently creates and lists webhooks but does not expose rotation or deletion.

See the API Reference for authentication boundaries and Backfill and recomputation for corrective emissions.

On this page