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, 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.
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
{
"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}. 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
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
$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
- Skema order & Try it: API Reference