# Webhook

> Terima permintaan HTTPS bertanda tangan saat penjualan, pesanan, stok, produk, atau pelanggan berubah, dan verifikasi di Node atau PHP.

Source: https://docs.bakasir.com/id/docs/developers/webhooks

Webhook adalah permintaan yang dikirim Bakasir ke server Anda saat ada perubahan, sehingga Anda tidak perlu terus memeriksa API. Tambahkan endpoint di **Pengaturan → Pengembang → Webhook**. Satu bisnis dapat memiliki hingga 20 endpoint, dan tiap endpoint memilih sendiri event yang diinginkan.

## Syarat endpoint

- Alamat harus diawali `https://` dan menuju server publik. Alamat jaringan privat, `localhost`, dan alamat metadata cloud ditolak.
- Bakasir tidak mengikuti pengalihan. Jawab tepat di alamat yang Anda simpan.
- Balas dengan status `2xx` apa pun dalam 10 detik. Selain itu dianggap gagal.

## Event

| Event | Dikirim saat |
| --- | --- |
| `sale.created` | Penjualan tercatat di kanal mana pun |
| `sale.updated` | Penjualan berubah, misalnya selesai, dibatalkan, atau direfund |
| `order.created` | Pesanan online dibuat |
| `order.updated` | Status atau pembayaran pesanan online berubah |
| `stock.updated` | Stok sebuah produk di outlet berubah |
| `product.created`, `product.updated`, `product.deleted` | Produk dibuat, diubah, atau dihapus |
| `customer.created`, `customer.updated`, `customer.deleted` | Pelanggan dibuat, diubah, atau dihapus |

Anda juga bisa berlangganan satu grup seperti `product.*`, atau semuanya dengan `*`. Perubahan di outlet Demo tidak pernah mengirim event. Beberapa perubahan pada data yang sama dalam satu operasi dikirim sebagai satu event.

## Permintaannya

Bakasir mengirim `POST` dengan isi JSON:

```json
{
  "id": "evt_5f2b0c7e91d34a0c8d1e6b27",
  "type": "product.updated",
  "created_at": "2026-11-04T08:30:00Z",
  "data": {
    "id": "01K9ZT6Q2X3M8N4V7B5C1D0EFG",
    "name": "Es Kopi Susu",
    "price": 24000
  }
}
```

`data` berbentuk sama dengan objek API yang sesuai (produk, penjualan beserta itemnya, pesanan online beserta barisnya, pelanggan). Untuk `stock.updated` isinya baris stok dengan `store_id`, `product_id`, `variant_id`, `qty`, dan `avg_cost`. Untuk data yang dihapus hanya berisi `id`. Data dibaca saat pengiriman pertama dicoba lalu disimpan, jadi percobaan ulang mengirim isi yang sama.

Header:

| Header | Nilai |
| --- | --- |
| `Bakasir-Event` | Tipe event |
| `Bakasir-Event-Id` | Id event, sama dengan `id` di isi. Pakai untuk mengabaikan duplikat |
| `Bakasir-Delivery` | Id pengiriman |
| `Bakasir-Attempt` | 1 untuk percobaan pertama, lalu 2, 3, dan seterusnya |
| `Bakasir-Signature` | `t=<waktu unix>,v1=<tanda tangan hex>` |

## Verifikasi tanda tangan

Setiap permintaan ditandatangani dengan secret endpoint, yang ditampilkan sekali saat dibuat atau diganti. Hitung HMAC SHA-256 dari teks `<t>.<isi mentah>` dengan secret itu, lalu bandingkan dengan `v1` secara waktu konstan. Pakai isi mentah persis seperti diterima, sebelum diurai sebagai JSON, dan tolak permintaan yang `t`-nya lebih dari lima menit lalu.

Node.js:

```js
import crypto from "node:crypto";

export function verifyBakasir(rawBody, header, secret, toleranceSeconds = 300) {
    const parts = Object.fromEntries(header.split(",").map((pair) => pair.split("=")));
    if (!parts.t || !parts.v1) return false;
    if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;
    const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
    const given = Buffer.from(parts.v1, "hex");
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
```

Dengan Express, baca isi mentah memakai `express.raw({ type: "application/json" })` lalu berikan `req.body.toString("utf8")`.

PHP:

```php
<?php

function verifyBakasir(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
    $parts = [];
    foreach (explode(",", $header) as $pair) {
        [$key, $value] = array_pad(explode("=", $pair, 2), 2, "");
        $parts[$key] = $value;
    }
    if (!isset($parts["t"], $parts["v1"]) || abs(time() - (int) $parts["t"]) > $tolerance) {
        return false;
    }
    $expected = hash_hmac("sha256", $parts["t"] . "." . $rawBody, $secret);
    return hash_equals($expected, $parts["v1"]);
}

$raw = file_get_contents("php://input");
$signature = $_SERVER["HTTP_BAKASIR_SIGNATURE"] ?? "";
if (!verifyBakasir($raw, $signature, getenv("BAKASIR_WEBHOOK_SECRET"))) {
    http_response_code(401);
    exit;
}
http_response_code(204);
```

## Percobaan ulang dan log pengiriman

Jika endpoint Anda gagal atau kehabisan waktu, Bakasir mencoba lagi 6 kali, jadi total hingga 7 percobaan dalam sekitar 21 jam: setelah 1 menit, 5 menit, 30 menit, 2 jam, 6 jam, dan 12 jam. Urutan tidak dijamin dan sebuah event bisa tiba lebih dari sekali, jadi pakai `Bakasir-Event-Id` untuk mengabaikan pengulangan dan `created_at` untuk mengabaikan kondisi yang lebih lama.

Setelah percobaan terakhir gagal, pengiriman ditandai gagal dan pemilik menerima notifikasi. Anda dapat mengulang pengiriman yang gagal secara manual.

**Pengaturan → Pengembang → Pengiriman** mencantumkan setiap pengiriman selama 30 hari beserta status, jumlah percobaan, jawaban server Anda, dan error terakhir. Pakai **Kirim tes** pada endpoint untuk mengirim event `webhook.test` bertanda tangan saat itu juga.

## Ganti atau hentikan

- **Ganti secret** langsung menerbitkan secret baru. Perbarui server Anda lebih dulu atau terima keduanya sebentar.
- Matikan endpoint untuk menjeda, atau hapus untuk menghentikan semua event dan menghapus lognya.
- Jika Anda mereset bisnis ke pengaturan pabrik, semua kunci dan endpoint dihapus.
