# Kit Webhooks

## When to Use This Skill

* How do I receive Kit (ConvertKit) webhooks?
* How do I verify the `X-Kit-Signature` header?
* How do I handle `subscriber.created`, `subscriber.activated`, `subscriber.unsubscribed`, or `subscriber.tag_added` events?
* Why is my Kit webhook signature verification failing, or failing only after a secret rotation?
* Why do my legacy Kit webhooks (`/v4/webhooks`) have no signature?

## Verification (core)

Kit signs deliveries from webhook endpoints (`/v4/webhook_endpoints`, or [app.kit.com/webhooks](https://app.kit.com/webhooks)) with an `X-Kit-Signature` header of the form `t=<unix seconds>,v1=<hex>`. During a secret rotation it carries one `v1` per valid secret, so check **every** `v1`.
`v1` is the hex HMAC-SHA256 of `"<t>.<raw body>"`, keyed with the endpoint secret. The secret starts with `whsec_` but is used **whole** as UTF-8 key bytes: don't strip the prefix or base64-decode it, and don't use the `standardwebhooks` or `svix` libraries (this is not Standard Webhooks). Legacy `/v4/webhooks` deliveries are not signed.

Kit suggests rejecting timestamps older than 5 minutes, but Hookdeck retries and replays keep the original `t`, so the window is opt-in here (`KIT_TIMESTAMP_TOLERANCE_SECONDS`, default `0`). Always sign over the **raw body**.

```javascript
const crypto = require('crypto');

// toleranceSeconds = 0 (default) skips the timestamp window; a positive value opts in
function verifyKitWebhook(rawBody, header, secret, toleranceSeconds = 0) {
  if (!header) return false;
  const parts = header.split(',').map((p) => p.trim().split('='));
  const t = parts.find(([k]) => k === 't')?.[1];
  const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || !/^\d+$/.test(t) || sigs.length === 0) return false;
  if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false; // opt-in replay window
  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
  return sigs.some((sig) => {
    try { return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); }
    catch { return false; } // length mismatch
  });
}

```

Kit's docs show Ruby, Node.js and Python snippets but no verification SDK, so verification is manual in every framework. In Express, read the raw body with `express.raw({ type: '*/*' })` so any content type verifies.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/kit-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/kit-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/kit-webhooks/examples/fastapi/).

## Common Event Types

The body is an envelope `{ "delivery_id": 123456, "events": [...] }` with 1 to 100 events of the same `type`. Iterate `events` and route on each event's `type`:

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `subscriber.created` | A subscriber is added to the account | Sync to CRM, welcome flows |
| `subscriber.activated` | A subscriber becomes `active` (confirmed) | Start onboarding |
| `subscriber.unsubscribed` | A subscriber unsubscribes | Suppress downstream messaging |
| `subscriber.bounced` | An address hard-bounces | Clean up contact records |
| `subscriber.complained` | A subscriber reports spam | Suppress and investigate |
| `subscriber.subscribed_to_form` | A subscriber joins a form | Attribute signup source |
| `subscriber.tag_added` | A tag is added to a subscriber | Segment, grant access |
| `subscriber.tag_removed` | A tag is removed from a subscriber | Revoke access |

More types (`subscriber.added_to_sequence`, `subscriber.sequence_completed`, `subscriber.custom_field_value_updated`, `tag.*`, `custom_field.*`, `sequence.*`, `broadcast.*`, `post.published`) are listed in [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/kit-webhooks/references/overview.md).

## Environment Variables

```bash
KIT_WEBHOOK_SECRET=whsec_xxxxx             # Returned once when the endpoint is created or its secret rotated
KIT_TIMESTAMP_TOLERANCE_SECONDS=0          # Optional replay window; a positive value opts in

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 kit --path /webhooks/kit

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/kit-webhooks/references/overview.md) - Kit webhook concepts, events, envelope shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/kit-webhooks/references/setup.md) - Create an endpoint and get the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/kit-webhooks/references/verification.md) - Signature verification details and gotchas