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:
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:
{
"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:
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.