> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kawan.digital/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Terima event siklus hidup order di sistem Anda. Delivery bertanda tangan HMAC, diantrikan di background — bukan bagian dari checkout.

Daftarkan URL HTTPS di [Dashboard → Pengaturan → Developer API](https://kawan.digital/dashboard/settings/developers), pilih event, lalu simpan signing secret (ditampilkan **sekali**). Produksi wajib HTTPS. `http://localhost` dan `http://127.0.0.1` diizinkan untuk development.

Webhook tersedia di paket **Growth** dan **Scale**, bersama [Public API](/api).

## Event

Tidak ada status `cancelled` atau `completed`. Gunakan `order.expired` / `order.failed` dan `order.paid`.

| Event                         | Kapan dikirim                                                             |
| ----------------------------- | ------------------------------------------------------------------------- |
| `order.created`               | Order baru dibuat (storefront, API, atau order gratis).                   |
| `order.updated`               | Status order berubah. Selalu ikut bersama event status spesifik di bawah. |
| `order.paid`                  | Order lunas. Ini setara "completed" di Kawan Digital.                     |
| `order.awaiting_verification` | Menunggu konfirmasi pembayaran (misalnya transfer manual).                |
| `order.failed`                | Pembayaran gagal.                                                         |
| `order.expired`               | Order pending kedaluwarsa.                                                |
| `order.refunded`              | Order di-refund.                                                          |

Saat status menjadi `paid`, Anda menerima **dua** delivery jika keduanya dipilih: `order.updated` dan `order.paid`. Proses masing-masing secara idempoten. Return URL payment gateway bukan bukti lunas — pembeli bisa kembali ke `redirect.return_url` saat status masih pending. Grant akses hanya setelah `order.paid` (atau GET order `status: paid`).

## Request

Setiap delivery adalah `POST` JSON. `User-Agent`: `KawanDigital-Webhooks/1.0`.

### Headers

| Header                | Isi                                                |
| --------------------- | -------------------------------------------------- |
| `X-Kawan-Event`       | Tipe event, misalnya `order.paid`.                 |
| `X-Kawan-Delivery-Id` | ID delivery. Pakai untuk idempotensi di sisi Anda. |
| `X-Kawan-Timestamp`   | Unix seconds saat delivery dikirim.                |
| `X-Kawan-Signature`   | `t={timestamp},v1={hmac_sha256_hex}`               |

### Body

```json theme={null}
{
  "id": "8c1a2b3d-4e5f-6789-abcd-ef0123456789",
  "type": "order.paid",
  "created_at": "2026-09-08T10:00:00.000Z",
  "data": {
    "order": {
      "id": "11111111-1111-4111-8111-111111111111",
      "status": "paid",
      "payment_reference": "INV-123",
      "amount": 99000,
      "subtotal": 99000,
      "currency": "IDR",
      "settlement_channel": "platform_gateway",
      "payment_provider": "duitku",
      "payment_method": "qris",
      "buyer": {
        "name": "Budi",
        "email": "budi@example.com",
        "phone": "081234567890"
      },
      "items": [
        {
          "type": "product",
          "title": "Ebook Starter",
          "quantity": 1,
          "unit_price": 99000,
          "line_total": 99000,
          "product_id": "22222222-2222-4222-8222-222222222222",
          "bundle_id": null
        }
      ],
      "expires_at": null,
      "created_at": "2026-09-08T09:55:00.000Z",
      "updated_at": "2026-09-08T10:00:00.000Z",
      "payment_url": null,
      "requires_payment": false
    }
  }
}
```

`id` di body adalah ID event. Objek `data.order` sama dengan response [GET /v1/orders/\{order\_id}](/api). `payment_url` hanya ada selama pembayaran masih diperlukan (`pending` / `awaiting_verification`).

## Verifikasi signature

Payload yang ditandatangani: `{timestamp}.{raw_body}` (body mentah, bukan JSON yang di-parse ulang). HMAC-SHA256, output hex, memakai signing secret webhook. Tolak jika timestamp lebih dari **300 detik** dari waktu server Anda.

Baca body sebagai raw bytes/string sebelum `JSON.parse`. Parsing lalu stringify ulang akan mengubah whitespace dan gagal verifikasi.

### Node.js

```javascript theme={null}
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(secret, header, rawBody, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => {
      const i = p.indexOf("=");
      return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
    })
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(v1, "utf8");
  const b = Buffer.from(expected, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

### PHP

```php theme={null}
$rawBody = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_KAWAN_SIGNATURE"] ?? "";
$parts = [];
foreach (explode(",", $header) as $piece) {
  [$k, $v] = array_map("trim", explode("=", $piece, 2) + [1 => ""]);
  $parts[$k] = $v;
}
$t = (int) ($parts["t"] ?? 0);
$v1 = $parts["v1"] ?? "";
if (!$t || $v1 === "") { http_response_code(401); exit; }
if (abs(time() - $t) > 300) { http_response_code(401); exit; }
$expected = hash_hmac("sha256", $t . "." . $rawBody, $secret);
if (!hash_equals($expected, $v1)) { http_response_code(401); exit; }
$event = json_decode($rawBody, true);
```

## Idempotensi

Simpan `X-Kawan-Delivery-Id` (atau `id` di body) yang sudah diproses. Delivery yang sama bisa dikirim ulang setelah timeout, retry, atau tombol retry di dashboard. Jangan anggap `order.id` unik per event — satu order memicu banyak event.

## Response & retry

* Timeout: 10 detik.
* Sukses: HTTP 2xx. Balas cepat, kerjakan job berat di antrian Anda.
* Di-retry: error jaringan, 408, 429, 5xx. Backoff `30s × 2^(attempts-1)`, maksimum 1 jam, maksimal 5 percobaan.
* 4xx lain (termasuk 401 dari signature yang salah) tidak di-retry.
* Delivery gagal bisa di-retry manual dari dashboard.

Worker berjalan lewat cron setiap 2 menit. Jangan mengandalkan webhook untuk menyelesaikan checkout pembeli.

## Lanjut

* Buat order lewat API: [Integrasi API](/integrasi)
* Skema order & Try it: [API Reference](/api)
