# Webhook

Kabari sistem kamu otomatis setiap ada pesanan, pendaftaran, pembatalan, atau check-in.

> Sumber: https://help.nyebar.id/organizer/integrasi/webhook

Dengan webhook, Nyebar mengirim kabar ke sistem kamu begitu sesuatu terjadi, misalnya untuk memasukkan pembeli ke CRM, mengirim pesan WhatsApp sambutan dari sistem sendiri, atau memperbarui papan skor kehadiran. Sistem kamu tidak perlu bertanya terus ke API.

Webhook diatur oleh owner atau Admin di menu **Integrasi**. Bagian akhir artikel ini ditujukan untuk developer yang menyiapkan penerimanya.

## Kejadian yang bisa dikirim

| Kejadian | Kapan dikirim |
| --- | --- |
| **Pesanan terkonfirmasi** (`order.confirmed`) | Pesanan dibayar (atau gratis) dan tiketnya terbit. Termasuk pendaftaran gratis yang kamu setujui. |
| **Pendaftaran masuk** (`registration.received`) | Pendaftaran baru menunggu persetujuan kamu. |
| **Pesanan batal** (`order.cancelled`) | Pendaftaran ditolak, atau pesanan kedaluwarsa atau gagal dibayar. |
| **Tiket check-in** (`ticket.checked_in`) | Tiket di-check-in, dari dasbor, link kru, maupun API. |

## Menambah webhook

1. Buka menu **Integrasi**, lalu di bagian **Webhook** klik **Tambah webhook**.

   ![Bagian Webhook di halaman Integrasi dengan daftar webhook dan tombol Tambah webhook.](https://help.nyebar.id/screenshots/id/org-webhooks.webp "Daftar webhook di halaman Integrasi.")

2. Isi **URL endpoint**, yaitu alamat di sistem kamu yang menerima kiriman. Alamatnya harus https dan bisa diakses dari internet.
3. Pilih **Kejadian yang dikirim**, lalu klik **Simpan**.

   ![Jendela Tambah webhook berisi URL endpoint, deskripsi, dan pilihan kejadian.](https://help.nyebar.id/screenshots/id/org-webhook-form.webp "Isi URL dan pilih kejadian yang ingin dikirim.")

4. Salin **secret** yang muncul dan pasang di sistem kamu untuk memverifikasi kiriman. Secret juga bisa dilihat lagi di halaman webhook.

Satu organizer bisa punya sampai 5 webhook.

## Memantau kiriman

Di daftar webhook, setiap baris punya dua ikon:

- **Riwayat kiriman** (ikon jam): kiriman 30 hari terakhir. Klik satu kiriman untuk melihat isi kiriman dan respons dari sistem kamu, lalu **Kirim ulang** kalau perlu.
- **Pengaturan** (ikon roda gigi): mengganti URL, deskripsi, atau kejadian lewat **Edit Endpoint**, menyalakan atau mematikan webhook sementara, melihat atau mengganti **Secret penandatanganan**, dan menghapus webhook.

Di bagian atas kedua halaman ada **Kirim tes**, untuk memastikan endpoint kamu menerima kiriman dengan benar.

![Riwayat kiriman sebuah webhook dengan status, kode respons, dan jumlah percobaan.](https://help.nyebar.id/screenshots/id/org-webhook-detail.webp "Riwayat kiriman sebuah webhook.")

Kalau sistem kamu tidak menjawab dengan sukses, Nyebar mencoba lagi otomatis setelah 1 menit, 5 menit, 30 menit, 2 jam, 6 jam, dan 24 jam. Kalau kiriman ke satu webhook terus gagal selama lebih dari sehari, webhook dimatikan otomatis dan owner serta Admin mendapat notifikasi di dasbor. Setelah endpoint diperbaiki, kirim tes lalu nyalakan lagi. Kejadian yang terjadi selama webhook mati tetap dikirim setelah dinyalakan, selama belum lewat 30 hari.

## Untuk developer

### Format kiriman

Nyebar mengirim `POST` berisi JSON ke URL kamu, dengan header:

| Header | Isi |
| --- | --- |
| `Nyebar-Event-Id` | ID kejadian, misalnya `evt_1042`. Sama untuk setiap percobaan ulang. |
| `Nyebar-Event-Type` | Jenis kejadian, misalnya `order.confirmed`. |
| `Nyebar-Signature` | Tanda tangan, `t=<waktu unix>,v1=<hex>`. |

```json
{
  "id": "evt_1042",
  "type": "order.confirmed",
  "created_at": "2026-10-11T14:36:46Z",
  "data": {
    "event": { "id": 134, "title": "Nusantara City Run 10K", "slug": "nusantara-city-run-10k" },
    "order": {
      "id": 1230,
      "code": "ORDRHMRQERV",
      "status": "confirmed",
      "buyer_name": "Andi Nasution",
      "buyer_email": "andi@contoh.com",
      "ticket_subtotal": 200000,
      "addon_subtotal": 0,
      "discount_amount": 0,
      "platform_fee_amount": 0,
      "total_price": 200000,
      "created_at": "2026-10-11T14:30:02Z"
    },
    "tickets": [
      {
        "id": 1583,
        "code": "NYE8KRXGKU8",
        "holder_name": "Andi Nasution",
        "holder_email": "andi@contoh.com",
        "ticket_type": { "id": 235, "name": "Reguler" },
        "bib_number": null,
        "checked_in_at": null,
        "created_at": "2026-10-11T14:36:46Z"
      }
    ]
  }
}
```

Isi `data` per kejadian:

- `order.confirmed`: `event`, `order`, dan `tickets` yang terbit.
- `registration.received`: `event` dan `order` (status `awaiting_approval`).
- `order.cancelled`: `event`, `order`, dan `reason`: `rejected` (ditolak), `expired` (batas waktu persetujuan atau pembayaran lewat), `payment_failed`, atau `payment_expired`.
- `ticket.checked_in`: `event`, `ticket`, dan `source`: `dashboard`, `crew_link`, `api`, atau `support`.

Bentuk `order` dan `ticket` sama dengan respons [API organizer](/organizer/integrasi/api-organizer).

### Menjawab kiriman

Jawab dengan status 2xx dalam 10 detik. Kalau sistem kamu butuh waktu lebih lama, simpan kirimannya dulu, jawab 200, lalu proses belakangan. Status selain 2xx, timeout, atau redirect dianggap gagal dan dicoba lagi.

Kiriman bisa datang lebih dari sekali (misalnya saat percobaan ulang) dan urutannya tidak dijamin. Gunakan `id` kejadian untuk melewati kiriman yang sudah pernah diproses, dan `created_at` untuk urutannya.

### Memverifikasi tanda tangan

Hitung HMAC-SHA256 dari `<t>.<body mentah>` dengan secret webhook, lalu bandingkan dengan nilai `v1`. Tolak juga kiriman yang `t`-nya lebih dari 5 menit dari waktu sekarang.

```js
// Node.js (Express): pakai body mentah, bukan hasil JSON.parse
const crypto = require("crypto");

app.post("/webhooks/nyebar", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Nyebar-Signature") || "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", process.env.NYEBAR_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  if (!fresh || !parts.v1 || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  // ... proses event ...
  res.sendStatus(200);
});
```

```php
<?php
// PHP
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_NYEBAR_SIGNATURE'] ?? ''), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $body, getenv('NYEBAR_WEBHOOK_SECRET'));
if (abs(time() - (int) ($parts['t'] ?? 0)) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) {
    http_response_code(400);
    exit;
}
$event = json_decode($body, true);
// ... proses event ...
http_response_code(200);
```

```python
# Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/webhooks/nyebar")
def nyebar_webhook():
    parts = dict(p.split("=", 1) for p in request.headers.get("Nyebar-Signature", "").split(",") if "=" in p)
    body = request.get_data()
    expected = hmac.new(os.environ["NYEBAR_WEBHOOK_SECRET"].encode(), parts.get("t", "").encode() + b"." + body, hashlib.sha256).hexdigest()
    if abs(time.time() - int(parts.get("t", 0))) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
        abort(400)
    event = request.get_json()
    # ... proses event ...
    return "", 200
```

Kalau secret diganti lewat **Ganti secret**, secret lama langsung tidak berlaku. Perbarui secret di sistem kamu segera setelahnya.
