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. Keep the secret on your server, for example 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

Kling only delivers webhooks to a public URL, so a server on localhost can't receive them directly. Use a tunnelling tool, such as ngrok or Cloudflare Tunnel, that gives your local server a public HTTPS address, and register that address as a webhook in test mode. Events then reach your machine signed, exactly as they will in production, so your signature check runs as it should.

To check the connection without making a payment, POST /v1/notification-channels/test-webhooks sends a signed debug.check event to every webhook you have. Or make a test payment with a test card and watch the real events arrive. When you are done, remove the tunnel's webhook, or switch it off, in the dashboard.

View as markdown