Receive Klik Flow events in your system.
Add an HTTPS URL in Settings › API & Webhook › Webhook (Pengaturan › API & Webhook › Webhook), or with POST /api/v2/webhooks, and choose its events. Klik Flow sends a JSON POST signed with that webhook's secret. The list of events and their contents is in the Webhook events guide.
Delivery shape
Every event is sent in the same shape:
{
"id": "evt_9Qx2Lm4Kp7Rt1Zs8",
"type": "message.status",
"created": "2026-11-01T09:13:10+07:00",
"business_id": 7,
"data": {
"message_id": 99140,
"conversation_id": 812,
"channel": "whatsapp_official",
"to": "6281234567890",
"status": "read",
"error": null,
"at": "2026-11-01T09:13:10+07:00"
}
}id: the event's unique id (evt_…), the same on every attempt. Store it, and ignore the event if it was already processed.created: when it happened, in your business's time zone, with its offset (+07:00).test: trueis present on test events and on the status of messages sent with a test key.- At most 5 webhooks per business; each webhook has its own secret.
Headers
Klikflow-Event: the event name, for examplemessage.received.Klikflow-Event-Id: the same asidin the body.Klikflow-Delivery-Attempt: the attempt number (1–7).Klikflow-Signature: t=<seconds>,v1=<HMAC-SHA256 of "<t>.<body>">, generated again on every attempt.
Verify the signature (Node.js)
import crypto from "node:crypto";
// rawBody: the request body exactly as received (string), before parsing.
function verifyKlikFlow(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // more than 5 minutes off: reject
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return parts.v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}Verify the signature (PHP)
<?php
$raw = file_get_contents("php://input");
$header = $_SERVER["HTTP_KLIKFLOW_SIGNATURE"] ?? "";
parse_str(str_replace(",", "&", $header), $parts);
$t = intval($parts["t"] ?? 0);
$expected = hash_hmac("sha256", $t . "." . $raw, getenv("KLIKFLOW_WEBHOOK_SECRET"));
if (!$t || abs(time() - $t) > 300 || !hash_equals($expected, $parts["v1"] ?? "")) {
http_response_code(400);
exit;
}
$event = json_decode($raw, true);
http_response_code(200);Verify the signature (Python)
import hashlib, hmac, time
def verify_klikflow(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t = int(parts.get("t", "0"))
if not t or abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Responding and retries
Respond with 2xx within 8 seconds, then do the processing in the background. Otherwise the delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 24 hours (7 attempts, about 33 hours). After that, the delivery goes to the failed list.
- Klik Flow does not follow redirects: a 3xx counts as a failure, so use the final URL.
- Public HTTPS addresses only; local network addresses are refused.
- Resend (Kirim ulang) or Resend all (Kirim ulang semua) in Settings › API & Webhook sends the same event (same id and content) as a new attempt.
- If every delivery fails for 3 days, the webhook is paused automatically and the reason appears on its card. Reactivate it after the receiver is fixed; waiting deliveries are retried right away.
- The delivery log (status, HTTP code, time, and content) is kept for 30 days.
Order and duplicates
Events can arrive out of order, and after a network problem they can arrive twice. Use id to ignore repeats, and the time in data (for example at in a message status) for ordering.
Test events
The Send test event button (Kirim event uji) on the webhook card, or POST /api/v2/webhooks/{id}/test, sends a sample of one event with test: true and shows your system's response right away. Test events are not retried when they fail and do not count toward the webhook's health.