Webhooks
Push events delivered to your HTTPS endpoint with HMAC-SHA256 signatures and automatic retry.
Overview
KobKlein delivers webhook events via HTTP POST to your registered endpoint. Your endpoint must respond with a 2xx status within 10 seconds. Events not acknowledged within that window are retried with exponential backoff.
Delivery method
HTTP POST, JSON body
Timeout
10 seconds
Max retries
5 (over 24 hours)
Signature
HMAC-SHA256 in X-KK-Signature
Event Catalog
Request Headers
Every webhook delivery includes these headers:
| Header | Value |
|---|---|
| Content-Type | application/json |
| X-KK-Event | payment.completed (the event type) |
| X-KK-EventId | evt_abc123 (idempotency key — deduplicate on this) |
| X-KK-Timestamp | 1783184640 (Unix seconds — reject if > 5 min old) |
| X-KK-Signature | HMAC-SHA256 hex digest (see below) |
| X-KK-Partner | your-partner-slug |
Signature Verification
Always verify the X-KK-Signature before processing a webhook. The signature is computed as:HMAC-SHA256(webhookSecret, timestamp + "." + rawBodyString)
import crypto from "crypto";
export function verifyWebhook(req, webhookSecret) {
const signature = req.headers["x-kk-signature"];
const timestamp = req.headers["x-kk-timestamp"];
const rawBody = req.rawBody; // must be the raw bytes/string
// Reject stale events (replay protection)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
throw new Error("Webhook timestamp too old");
}
const expected = crypto
.createHmac("sha256", webhookSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
throw new Error("Invalid webhook signature");
}
}import hmac, hashlib, time
def verify_webhook(request, webhook_secret: str):
signature = request.headers.get("X-KK-Signature", "")
timestamp = request.headers.get("X-KK-Timestamp", "0")
raw_body = request.get_data(as_text=True)
if abs(time.time() - int(timestamp)) > 300:
raise ValueError("Webhook timestamp too old")
expected = hmac.new(
webhook_secret.encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise ValueError("Invalid webhook signature")Retry Logic
If your endpoint returns a non-2xx status or times out, KobKlein retries with exponential backoff:
| Attempt | Delay after previous |
|---|---|
| 1 (initial) | — |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| — | Event dropped after attempt 5 |
Deduplicate on X-KK-EventId — the same ID is used across all retry attempts for a given event.
Testing Webhooks
Use a test API key (kk_test_) to trigger test events without real money movement. You can also use webhook.site or ngrok to inspect deliveries locally.
# Register a test endpoint pointing to your local server
curl https://api.kobklein.com/v1/partner/webhooks \
-H "X-API-Key: kk_test_your_test_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-ngrok-id.ngrok.io/webhooks/kobklein",
"events": ["payment.completed", "kyc.updated"],
"secret": "local_test_secret_123"
}'Try it in the Sandbox Console
Use the Sandbox Console to trigger test webhook events and inspect the payloads directly in your browser — no local server required.

