Topics

DocsDeveloper reference

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.

View as markdown