# Setup webhook

> Daftarkan URL webhook di dashboard dan siapkan server yang menerima event dari CSKU.


Webhook membuat CSKU mengirim `POST` ke server Anda setiap kali ada pesan masuk dari pelanggan, balasan AI, atau balasan agen. Anda tidak perlu polling ke [Ambil pesan](https://docs.csku.ai/api/ambil-pesan).

## Prasyarat

- Paket Anda punya fitur **Webhook**. Jika tidak, menu Webhook menampilkan ajakan upgrade. Lihat [Berlangganan & kredit](https://docs.csku.ai/panduan/berlangganan).
- Anda login sebagai **owner** bisnis. OTP hanya dikirim ke email owner.
- **Secret Key** sudah dibuat di **Pengaturan → Secret Key**. Nilai ini dikirim di header `Authorization` setiap request webhook. Lihat [Autentikasi](https://docs.csku.ai/api/autentikasi).
- Server Anda bisa diakses publik lewat `https`. Dashboard menolak URL `http`, `localhost`, dan alamat IP privat.

## Daftarkan URL

1. Buka **Pengaturan → Webhook**.
2. Isi **Webhook URL**, misalnya `https://api.toko-anda.com/csku/webhook`.
3. Klik **Perbarui Webhook**, lalu konfirmasi. CSKU mengirim kode OTP 6 digit ke email owner. Kode berlaku 5 menit.
4. Masukkan kode OTP, lalu klik **Perbarui Webhook**.

Setelah tersimpan, semua event langsung dikirim ke URL tersebut.

> **Catatan: Satu URL, semua event**
>
> Setiap bisnis punya satu URL webhook. Semua event dikirim ke URL itu dan tidak bisa difilter dari dashboard. Abaikan event yang tidak Anda perlukan di server Anda. Daftar event ada di [Event & payload](https://docs.csku.ai/webhook/event).

## Contoh penerima

Server Anda wajib:

1. Mencocokkan header `Authorization` dengan Secret Key Anda.
2. Membalas body JSON `{"success": true}` secepatnya (di bawah 30 detik).

Status HTTP tidak dibaca. Yang menentukan sukses hanya field `success` di body. Detail ada di [Keamanan & retry](https://docs.csku.ai/webhook/keamanan-retry).

### Node.js (Express)

```js
import express from 'express'
import crypto from 'node:crypto'

const app = express()
app.use(express.json())

const SECRET = process.env.CSKU_SECRET

function isValid(header) {
  const a = Buffer.from(header || '')
  const b = Buffer.from(SECRET)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

app.post('/csku/webhook', (req, res) => {
  if (!isValid(req.get('Authorization'))) {
    return res.status(401).json({ success: false })
  }

  res.json({ success: true })

  // Proses setelah membalas
  const { event, message } = req.body
  console.log(event, message.id)
})

app.listen(3000)
```

### PHP

```php
<?php
$secret = getenv('CSKU_SECRET');
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

header('Content-Type: application/json');

if (!hash_equals($secret, $auth)) {
    http_response_code(401);
    echo json_encode(['success' => false]);
    exit;
}

$payload = json_decode(file_get_contents('php://input'), true);

// Simpan ke antrean, lalu balas
echo json_encode(['success' => true]);
```

> **Tips: Header Authorization di Apache**
>
> Beberapa konfigurasi Apache + PHP-FPM tidak meneruskan header `Authorization`. Jika `$_SERVER['HTTP_AUTHORIZATION']` kosong, tambahkan `SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1` di `.htaccess`.

### Python (Flask)

```python
import hmac
import os
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = os.environ["CSKU_SECRET"]

@app.post("/csku/webhook")
def csku_webhook():
    auth = request.headers.get("Authorization", "")
    if not hmac.compare_digest(auth, SECRET):
        return jsonify(success=False), 401

    payload = request.get_json(silent=True) or {}
    # Kirim ke antrean / background job
    print(payload.get("event"), payload.get("message", {}).get("id"))

    return jsonify(success=True)
```

### Go

```go
package main

import (
	"crypto/subtle"
	"encoding/json"
	"net/http"
	"os"
)

var secret = []byte(os.Getenv("CSKU_SECRET"))

func webhook(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	auth := []byte(r.Header.Get("Authorization"))
	if subtle.ConstantTimeCompare(auth, secret) != 1 {
		w.WriteHeader(http.StatusUnauthorized)
		w.Write([]byte(`{"success":false}`))
		return
	}

	var payload map[string]any
	if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
		w.Write([]byte(`{"success":false}`))
		return
	}

	// Proses di goroutine / antrean
	w.Write([]byte(`{"success":true}`))
}

func main() {
	http.HandleFunc("/csku/webhook", webhook)
	http.ListenAndServe(":3000", nil)
}
```

## Uji saat development

Belum ada tombol "kirim webhook uji". Untuk mencoba:

1. Pakai URL publik sementara:
   - [webhook.site](https://webhook.site) untuk melihat isi request tanpa menulis kode. Ingat: webhook.site tidak membalas `{"success": true}` secara default, jadi CSKU akan mencoba ulang.
   - Tunnel ke server lokal, misalnya `ngrok http 3000` atau `cloudflared tunnel --url http://localhost:3000`. Daftarkan URL `https` yang diberikan tunnel.
2. Picu event sungguhan: kirim pesan ke salah satu channel Anda (misalnya lewat Webchat atau WhatsApp). Anda akan menerima `channel.message_in`, lalu `ai.message_generated` jika AI agent aktif.
3. Balas dari menu **Chat** di dashboard untuk menerima `agent.message_out`.

> **Peringatan: Ganti URL setelah selesai**
>
> URL tunnel biasanya berubah setiap kali dijalankan ulang. Daftarkan URL produksi sebelum go-live, agar event tidak terkirim ke alamat yang sudah mati.

## Reset webhook

Buka **Pengaturan → Webhook**, klik **Reset**, lalu konfirmasi. URL dihapus dan CSKU berhenti mengirim event. Untuk mengaktifkan lagi, daftarkan URL baru dengan OTP.

## Selanjutnya

- [Event & payload](https://docs.csku.ai/webhook/event): Daftar event dan isi lengkap payload.
- [Keamanan & retry](https://docs.csku.ai/webhook/keamanan-retry): Verifikasi request, aturan sukses, dan jadwal retry.
