# 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.

Sumber: https://klikflow.id/docs/en/api/webhooks

## 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: true` is 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 example `message.received`.
- `Klikflow-Event-Id`: the same as `id` in 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.
