# Webhooks

Kling sends a signed webhook to your server when a payment, claim or subscription changes. How to register an endpoint, which events exist, how to verify the X-Kling-Signature header and how retries work.

A webhook is a request Kling sends to your server when something happens, such as a payment going through. Use webhooks to fulfil orders, not the customer's browser: the browser can close before it gets back to your site.

## Register an endpoint

In the dashboard, open **Notifications** and add a webhook with your URL and the events you want. Or use the API:

```bash
curl -X POST https://api.kling.is/v1/notification-channels \
  -H "Authorization: Bearer $KLING_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "webhook",
    "config": { "url": "https://example.is/api/webhooks/kling" },
    "event_subscriptions": ["checkout.session.completed", "payment_intent.captured"]
  }'
```

Kling generates a signing secret and returns it in `config.secret`; the dashboard shows it too. The URL must be publicly reachable. `npx @klingis/cli init` can do all of this for you and write the secret to `.env.local` as `KLING_WEBHOOK_SECRET`.

Test and live have separate endpoints and secrets.

## Events

| Event | When |
|---|---|
| `checkout.session.completed` | A customer finished a checkout session |
| `payment_intent.captured` | Money was taken: a card payment, or a bank claim that was paid |
| `payment_intent.failed` | A payment failed |
| `payment_intent.refunded` | A payment was refunded |
| `claim.created`, `claim.paid` | An online bank claim was issued or paid |
| `subscription.created`, `subscription.activated`, `subscription.updated`, `subscription.canceled` | Subscription changes |
| `subscription.renewed` | A renewal was charged |
| `subscription.trial_ended` | A free trial ended |
| `subscription.payment_failed`, `subscription.payment_retry_failed`, `subscription.payment_recovered` | A renewal failed, a retry failed, or a retry succeeded |

## The request

Kling sends a `POST` with a JSON body:

```json
{
  "id": "evt_...",
  "type": "payment_intent.captured",
  "created_at": "2026-10-01T12:00:00.000Z",
  "data": { "id": "pi_..." },
  "merchant_id": "...",
  "environment": "test"
}
```

`data` holds the object the event is about, starting with its `id`. Headers: `X-Kling-Signature`, `X-Kling-Event` (the event type) and `X-Kling-Delivery-Id`.

## Verify the signature

`X-Kling-Signature` is `sha256=` followed by the hex HMAC-SHA256 of the raw request body, keyed with your signing secret. Compute it over the exact bytes you received, before parsing the JSON:

```typescript
import crypto from "node:crypto";

export function verifyKlingSignature(rawBody: string, header: string | null, secret: string): boolean {
  if (!header) return false;
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

Reject the request with a 401 if it doesn't match.

## Retries

Answer with any 2xx status within 30 seconds. Anything else counts as a failure and Kling tries again after 1 minute, 5 minutes, 30 minutes and 2 hours (five attempts in all). Redirects are not followed. Make your handler safe to run twice for the same event: use the event `id` to skip duplicates.

`POST /v1/notification-channels/test-webhooks` sends a `debug.check` event to your endpoint so you can test it.

## Local development

`npx @klingis/cli listen --forward http://localhost:3000/api/webhooks/kling` fetches your test events and forwards them to your local server, with no public URL. Forwarded events are not signed, so skip the signature check for them in development.
