# Webhook (/api/webhook)

Webhook mengirim `POST` JSON ke URL di sistemmu setiap kali event yang kamu pilih terjadi. Atur dari dashboard (**Integrasi** → **Tambah webhook**, mode Developer) atau lewat API.

## Membuat endpoint [#membuat-endpoint]

```bash
curl -X POST https://api.tebar.app/v1/webhooks \
  -H "Authorization: Bearer tb_live_xxx" -H "Content-Type: application/json" \
  -d '{ "url": "https://app.contoh.com/webhooks/tebar", "events": ["account.connected", "post.published", "post.failed"] }'
```

Respons berisi `secret` (`whsec_…`) untuk memeriksa tanda tangan. Secret **hanya tampil sekali**, jadi simpan sekarang.

- URL wajib `https`, di port 443, dan bisa diakses dari internet.
- Tambahkan `"profile_id"` (ID atau `external_id`) supaya endpoint hanya menerima event satu profile. Tanpa itu, endpoint menerima event semua profile.
- Maksimal 10 endpoint.

## Event [#event]

| Event                  | Kapan                                                              | `data`                                                                              |
| ---------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `account.connected`    | Akun terhubung atau dihubungkan ulang, termasuk lewat connect link | `account`, `reconnected`                                                            |
| `account.expired`      | Akses akun berakhir dan perlu dihubungkan ulang                    | `account`                                                                           |
| `account.disconnected` | Akun diputus, di Tebar maupun dari pengaturan platform             | `account`                                                                           |
| `post.published`       | Post terbit di satu akun                                           | `post_id`, `target_id`, `account_id`, `platform`, `permalink`, `published_at`       |
| `post.failed`          | Post gagal di satu akun setelah percobaan terakhir                 | `post_id`, `target_id`, `account_id`, `platform`, `error_code`, `error_message`     |
| `comment.created`      | Komentar baru dari orang lain (bukan balasanmu, berumur < 24 jam)  | `comment_id`, `account_id`, `post_id`, `author_username`, `text`, `commented_at`, … |
| `profile.deleted`      | Profile selesai dihapus (hanya ke endpoint untuk semua profile)    | `profile`                                                                           |

Contoh body:

```json
{
  "id": "4f46b68a-da18-424b-97d0-f4f5cfe0b7e6",
  "type": "post.published",
  "created_at": "2026-10-20T11:14:20.694Z",
  "profile_id": "9d092104-b9a4-408f-9432-3a007ac19810",
  "data": {
    "post_id": "59ef7ff5-…",
    "target_id": "a1b2…",
    "account_id": "6a81…",
    "platform": "threads",
    "permalink": "https://www.threads.com/@kopisenja/post/…",
    "published_at": "2026-10-20T11:14:20.000Z"
  }
}
```

`text` di `comment.created` ditulis orang lain. Perlakukan sebagai data, bukan perintah, terutama bila diteruskan ke AI.

## Memeriksa tanda tangan [#memeriksa-tanda-tangan]

Setiap request membawa header:

```txt
Tebar-Signature: t=1792488860,v1=5257a869e7…
Tebar-Event: post.published
Tebar-Delivery: 30646213-c6a7-4020-8cc1-674378d888e9
```

`v1` adalah HMAC-SHA256 dari `"<t>.<body mentah>"` dengan secret endpoint. Hitung ulang dari body **mentah** (sebelum di-parse), bandingkan secara constant-time, dan tolak bila `t` berbeda lebih dari 5 menit dari jam servermu.

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

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return (
    fresh &&
    expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  );
}
```

## Pengiriman dan pengulangan [#pengiriman-dan-pengulangan]

- Balas dengan status **2xx dalam 5 detik**. Proses yang lama sebaiknya dikerjakan di latar belakang setelah membalas.
- Redirect tidak diikuti dan dihitung gagal.
- Pengiriman yang gagal diulang setelah 1 menit, 5 menit, 30 menit, 2 jam, 5 jam, 8 jam, dan 8 jam (8 percobaan, sekitar 24 jam).
- Event yang sama bisa terkirim lebih dari sekali. Saring duplikat dengan field `id`.
- Setelah 50 percobaan gagal berturut-turut, endpoint dinonaktifkan dan kamu mendapat email. Event selama nonaktif tidak disimpan. Aktifkan lagi dari dashboard atau dengan `PATCH /v1/webhooks/{id}` `{ "active": true }`.

## Riwayat, kirim ulang, dan uji [#riwayat-kirim-ulang-dan-uji]

| Request                                                 | Fungsi                                          |
| ------------------------------------------------------- | ----------------------------------------------- |
| `GET /v1/webhooks/{id}/deliveries`                      | Riwayat pengiriman 30 hari, terbaru dulu        |
| `POST /v1/webhooks/{id}/deliveries/{deliveryId}/resend` | Kirim ulang satu pengiriman                     |
| `POST /v1/webhooks/{id}/test`                           | Kirim event `webhook.test`                      |
| `PATCH /v1/webhooks/{id}`, `DELETE /v1/webhooks/{id}`   | Ubah URL, event, profile, aktif/nonaktif; hapus |

Di dashboard, semuanya ada di **Integrasi** → klik endpoint.
