Dokumentasi / AI Box Dan Software Analitik Video / Webhook
Webhook
Terima peristiwa deteksi, ringkasan berkala dan peringatan dari AI Box di sistem Anda sendiri, verifikasi tanda tangannya, dan tangani percobaan ulang.
AI Box dapat mengirim data ke sistem Anda saat kejadian berlangsung, sehingga Anda tidak perlu menanyakan API berulang
kali. Setiap pesan adalah POST HTTPS dengan isi JSON, ditandatangani dengan cara
Standard Webhooks.
Dua Jenis Webhook
- Webhook untuk integrator milik akun API. Satu langganan memilih jenis peristiwa produk dan lokasinya, lalu menerima peristiwa deteksi dalam batch, ringkasan berkala dan peringatan. Panduan ini terutama membahas jenis ini.
- Webhook peringatan milik lokasi. Admin menambahkannya di dasbor (halaman Webhook) atau dengan
POST /api/v1/sites/{id}/webhooks, dan setiap peringatan langsung dikirim, misalnya ke sistem manajemen video atau palang pintu. Webhook ini ditandatangani dengan cara yang sama.
Membuat Langganan
Langganan milik akun API, dan apa yang dikirimnya mengikuti akun itu: hanya lokasinya, serta nomor polisi, nama dan foto
hanya bila akun memiliki cakupan personal_data. Pemilik perangkat atau Admin lokasi akun dapat membuatnya di dasbor
(halaman Akun API, Webhook), atau sistem Anda dapat membuat langganannya sendiri dengan token akun dan cakupan
write.
Pertama, tampilkan jenis peristiwa produk perangkat:
curl --cacert box.pem \
-H "Authorization: Bearer <API_KEY>" \
"https://<BOX_HOST>/api/v1/webhook-event-types"
Lalu daftarkan penerima Anda:
curl --cacert box.pem \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name": "ERP receiver", "url": "https://<RECEIVER_HOST>/hooks/arsa", "event_types": ["traffic.passage", "traffic.speeding"], "sites": [1], "summary": "1h", "alerts": true}' \
"https://<BOX_HOST>/api/v1/webhook-subscriptions"
Jawabannya memuat langganan dan rahasia penandatanganannya, whsec_ diikuti teks base64. Rahasia hanya ditampilkan di
jawaban ini: simpan sebagai rahasia di penerima Anda. POST /api/v1/webhook-subscriptions/{id}/secret membuat rahasia
baru, dan yang lama langsung berhenti berlaku.
| Kolom | Arti |
|---|---|
url |
Penerima Anda, https:// atau http://. Alamat di dalam perangkat ditolak; jaringan privat hanya bila pemilik perangkat mengizinkannya |
event_types |
Peristiwa deteksi yang dikirim; [] tidak mengirim apa pun |
sites |
Id lokasi akun yang disertakan; [] berarti semuanya |
batch_seconds, batch_max |
Batch dikirim setelah batch_seconds (bawaan 10) atau begitu berisi batch_max peristiwa (bawaan 100), mana yang lebih dulu |
summary |
Ringkasan berkala: 15m, 1h, 1d, atau null untuk tanpa ringkasan |
alerts |
Apakah peringatan akun juga dikirim |
active |
false menjeda langganan; setelah dilanjutkan, pengiriman berlaku sejak saat itu |
Satu akun dapat memiliki paling banyak 10 langganan. POST /api/v1/webhook-subscriptions/{id}/test mengirim pesan
test bertanda tangan sekarang dan melaporkan jawaban penerima Anda. Pesan uji itu tetap ada di log pengiriman
langganan sebagai Uji, beserta jawaban penerima Anda; pesan itu tidak dikirim ulang.
Jenis Peristiwa
| Produk | Jenis peristiwa |
|---|---|
| Basic Safety Guard | bsg.violation, bsg.hazard_entry |
| Traffic Monitor | traffic.passage, traffic.speeding, traffic.wrong_way, traffic.stopped, traffic.congestion, traffic.watchlist |
| Smart Retail Counter | retail.visit, retail.crossing, retail.queue, retail.floor, retail.vip |
| DOOH Audience Meter | dooh.view, dooh.play |
GET /api/v1/webhook-event-types menampilkannya beserta labelnya dan kolom yang merupakan data pribadi (personal).
Kolom itu bernilai null untuk akun tanpa personal_data. Untuk Basic Safety Guard, nama pekerja dan bukti hanya
dikirim ke akun yang juga Admin di lokasi peristiwa, dan hanya selama pemilik lokasi mengaktifkan akuntabilitas.
Untuk Smart Retail Counter, retail.floor adalah waktu singgah di lantai toko per menit dan per kamera: area lantai,
kisinya dan detik yang dihabiskan di setiap sel, tanpa data pribadi. retail.vip adalah kedatangan VIP beserta nama VIP
tersebut. Peristiwa ini menyangkut seseorang (personal_only dalam daftar), jadi hanya dikirim ke akun dengan
personal_data, dan hanya selama pemilik toko mengaktifkan pengenalan staf dan VIP; akun tanpa personal_data tidak
menerima peristiwa ini sama sekali.
Pesan
Setiap POST membawa tiga header dan isi JSON:
| Header | Arti |
|---|---|
webhook-id |
Id pesan, sama pada setiap percobaan ulang pesan ini |
webhook-timestamp |
Waktu percobaan ini ditandatangani, dalam detik Unix |
webhook-signature |
v1, diikuti tanda tangan HMAC-SHA256 dalam base64 (lihat di bawah) |
Isinya adalah amplop dengan type pesan, timestamp (RFC 3339) dan data-nya:
type |
Dikirim | data |
|---|---|---|
events.batch |
Setiap batch_seconds atau batch_max peristiwa |
subscription_id, account, personal_data, product, count dan events; setiap peristiwa memiliki id, type, time, site_id, camera_id dan data-nya |
summary |
Setiap 15 menit, jam atau hari di zona waktu setiap lokasi | subscription_id, account, product, site_id, site, timezone, period, from, to dan figures, yaitu ikhtisar produk untuk periode itu |
alert.raised |
Saat peringatan muncul | Peringatan, seperti yang dikirim webhook peringatan |
test |
Pada POST .../test |
subscription_id, product |
id setiap peristiwa bersifat tetap, sehingga penerima dapat membuang peristiwa yang sudah disimpannya.
Contoh Isi Pesan
Semua nilai di bawah ini hanya contoh. Contoh ini untuk akun tanpa personal_data, sehingga kolom data pribadi bernilai null.
Satu batch event Traffic Monitor:
{
"type": "events.batch",
"timestamp": "2026-11-06T03:15:10.000Z",
"data": {
"subscription_id": 4,
"account": "erp-receiver",
"personal_data": false,
"product": "traffic",
"count": 2,
"events": [
{
"id": "01JC8Q2W7K3M5N9P4R6T8V0X2Z",
"type": "traffic.passage",
"time": "2026-11-06T03:15:02.412Z",
"site_id": 1,
"camera_id": 3,
"data": {"track": "3-118", "line": "count", "direction": "a_to_b", "lane": "L1", "class": "car", "colour": "white", "speed_kmh": 52.0, "plate": null, "photo": null}
},
{
"id": "01JC8Q2X1B4D6F8H0J2L4N6Q8S",
"type": "traffic.speeding",
"time": "2026-11-06T03:15:06.090Z",
"site_id": 1,
"camera_id": 3,
"data": {"track": "3-121", "lane": "L2", "class": "motorcycle", "speed_kmh": 81.0, "limit_kmh": 60, "plate": null}
}
]
}
}
Ringkasan per jam; figures berisi angka yang sama dengan ringkasan (overview) produk untuk jam tersebut:
{
"type": "summary",
"timestamp": "2026-11-06T03:01:00.000Z",
"data": {
"subscription_id": 4,
"account": "erp-receiver",
"product": "traffic",
"site_id": 1,
"site": "<SITE_NAME>",
"timezone": "Asia/Jakarta",
"period": "1h",
"from": "2026-11-06T02:00:00.000Z",
"to": "2026-11-06T03:00:00.000Z",
"figures": {"vehicles": 1184, "by_class": {"car": 540, "motorcycle": 489, "truck": 96, "bus": 37, "bicycle": 22}, "flow_per_min": 19.6, "speeds_measured": 980, "mean_speed_kmh": 47.3, "p85_speed_kmh": 58.0, "plates_read_pct": 0.0, "incidents": {"speeding": 12, "wrong_way": 0, "stopped": 1, "congestion": 0, "watchlist": 0}, "congested_now": []}
}
}
Sebuah peringatan, dengan data yang sama seperti yang dikirim webhook peringatan (di sini dari Basic Safety Guard):
{
"type": "alert.raised",
"timestamp": "2026-11-06T03:16:41.000Z",
"data": {
"id": 57,
"site_id": 1,
"site": "<SITE_NAME>",
"camera_id": 2,
"camera": "<CAMERA_NAME>",
"kind": "ppe.missing",
"severity": "critical",
"first": "2026-11-06T03:16:39.200Z",
"event_id": "01JC8Q5A9C2E4G6J8L0N2Q4S6U",
"details": {"missing": ["hard_hat"], "area": "<AREA_NAME>"},
"link": "https://<BOX_HOST>/alerts/inbox"
}
}
Kolom data setiap jenis event tercantum di GET /api/v1/webhook-event-types; referensi API produk menjelaskan kolom yang sama untuk log-nya.
Verifikasi Tanda Tangan
Tanda tangan adalah HMAC-SHA256 dalam base64 atas <webhook-id>.<webhook-timestamp>.<body>, dibuat dengan byte rahasia:
dekode teks base64 setelah whsec_. Periksa pada isi mentah persis seperti yang diterima, sebelum Anda mengurai JSON-nya.
Header dapat memuat beberapa tanda tangan yang dipisahkan spasi; terima pesan bila salah satunya cocok. Tolak stempel
waktu yang selisihnya lebih dari 5 menit dari jam Anda, agar pesan yang terekam tidak dapat diputar ulang di kemudian
hari.
Python, hanya dengan pustaka standar:
import base64
import hashlib
import hmac
import time
TOLERANCE_S = 5 * 60
def verify(secret, msg_id, timestamp, signatures, body):
"""True when the webhook-signature header holds a valid signature of the raw body (bytes)."""
try:
if abs(time.time() - int(timestamp)) > TOLERANCE_S:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
except ValueError:
return False
signed = msg_id.encode() + b"." + timestamp.encode() + b"." + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for sig in signatures.split(" "):
version, _, value = sig.partition(",")
if version == "v1" and hmac.compare_digest(value, expected):
return True
return False
Penerima dengan server HTTP dari pustaka standar:
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ["WEBHOOK_SECRET"]
seen = set() # use your database in production
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
h = self.headers
if not verify(SECRET, h.get("webhook-id", ""), h.get("webhook-timestamp", "0"),
h.get("webhook-signature", ""), body):
self.send_response(401)
self.end_headers()
return
if h["webhook-id"] not in seen:
seen.add(h["webhook-id"])
message = json.loads(body)
print(message["type"], message["data"]) # hand it to your system here
self.send_response(204)
self.end_headers()
HTTPServer(("", 8080), Receiver).serve_forever()
JavaScript, hanya dengan modul crypto Node.js:
const crypto = require("node:crypto");
const TOLERANCE_S = 5 * 60;
// True when the webhook-signature header holds a valid signature of the raw body (a Buffer).
function verify(secret, msgId, timestamp, signatures, body) {
if (!msgId || !timestamp || !signatures) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_S) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto
.createHmac("sha256", key)
.update(`${msgId}.${timestamp}.`)
.update(body)
.digest();
return signatures.split(" ").some((sig) => {
const [version, value] = sig.split(",");
if (version !== "v1" || !value) return false;
const given = Buffer.from(value, "base64");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
Penerima dengan modul http Node.js:
const http = require("node:http");
const SECRET = process.env.WEBHOOK_SECRET;
const seen = new Set(); // use your database in production
http
.createServer((req, res) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const body = Buffer.concat(chunks);
const h = req.headers;
if (!verify(SECRET, h["webhook-id"], h["webhook-timestamp"], h["webhook-signature"], body)) {
res.writeHead(401).end();
return;
}
if (!seen.has(h["webhook-id"])) {
seen.add(h["webhook-id"]);
const message = JSON.parse(body);
console.log(message.type, message.data); // hand it to your system here
}
res.writeHead(204).end();
});
})
.listen(8080);
Jawaban, Percobaan Ulang Dan Pengulangan
- Jawab dengan status 2xx dalam beberapa detik untuk menerima pesan. Simpan pesannya dan kerjakan sesudahnya.
- Percobaan ulang: jawaban
408,425,429atau5xx, atau tanpa jawaban, dicoba lagi dengan jeda yang makin panjang: 30 detik, berlipat dua hingga satu jam, atau waktu di headerRetry-AfterAnda. Pesan tetap berurutan: pesan berikutnya menunggu di belakang pesan yang sedang dicoba ulang. - Setelah 24 jam pesan yang masih gagal dihentikan, dan
healthlangganan menunjukkanfailing. - Jawaban lain:
4xxlainnya, atau pengalihan, langsung menggagalkan pesan. Pengalihan tidak pernah diikuti. - Log pengiriman (
GET /api/v1/webhook-subscriptions/{id}/deliveries) menampilkan setiap pesan dan jawaban terakhirnya;POST /api/v1/webhook-deliveries/{id}/retrymengirim lagi pesan yang gagal atau menunggu dalam 24 jam tersebut. - Paling sedikit satu kali: pesan dapat tiba dua kali, misalnya setelah perangkat menyala ulang di tengah pengiriman.
Pesan itu tetap membawa
webhook-idyang sama, jadi buang yang sudah pernah Anda terima.
Tidak ada yang dikirim selama akun API dicabut, kedaluwarsa atau tanpa cakupan read.
Menandatangani Permintaan Ke Perangkat
Tiga umpan berjalan ke arah sebaliknya: sistem Anda mengirim ke perangkat dan menandatangani permintaannya dengan kunci lokasi, dalam bentuk Standard Webhooks yang sama. Admin lokasi membuat kuncinya di dasbor; kunci ditampilkan satu kali.
| Umpan | Operasi | Kunci |
|---|---|---|
| Daftar pantau nomor polisi dari sistem lain | POST /api/traffic/v1/feeds/watchlist/{site} |
Kunci daftar pantau lokasi |
| Transaksi dari kasir (POS) toko | POST /api/retail/v1/pos/{site} |
Kunci POS lokasi |
| Log tayang dari CMS atau pemutar | POST /api/dooh/v1/feeds/plays/{site} |
Kunci log tayang lokasi |
Kirim webhook-id baru untuk setiap permintaan (id yang berulang ditolak), waktu saat ini sebagai webhook-timestamp
(harus dalam 5 menit dari jam perangkat), dan tanda tangan atas isi persis yang Anda kirim.
Python:
import base64
import hashlib
import hmac
import time
import uuid
def sign(key, body):
"""The three headers for a request to the box; key is the site's whsec_ key, body the bytes you send."""
msg_id = "msg_" + uuid.uuid4().hex
timestamp = str(int(time.time()))
secret = base64.b64decode(key.removeprefix("whsec_"))
signed = msg_id.encode() + b"." + timestamp.encode() + b"." + body
signature = base64.b64encode(hmac.new(secret, signed, hashlib.sha256).digest()).decode()
return {"webhook-id": msg_id, "webhook-timestamp": timestamp, "webhook-signature": "v1," + signature}
JavaScript:
const crypto = require("node:crypto");
// The three headers for a request to the box; key is the site's whsec_ key, body the string you send.
function sign(key, body) {
const msgId = "msg_" + crypto.randomUUID().replaceAll("-", "");
const timestamp = String(Math.floor(Date.now() / 1000));
const secret = Buffer.from(key.replace(/^whsec_/, ""), "base64");
const signature = crypto.createHmac("sha256", secret).update(`${msgId}.${timestamp}.${body}`).digest("base64");
return { "webhook-id": msgId, "webhook-timestamp": timestamp, "webhook-signature": `v1,${signature}` };
}
Contohnya, pembaruan daftar pantau:
BODY='{"upsert": [{"plate": "B 1234 XYZ", "reason": "tax_arrears"}], "remove": ["D 5678 AB"]}'
curl --cacert box.pem \
-H "Content-Type: application/json" \
-H "webhook-id: <MESSAGE_ID>" \
-H "webhook-timestamp: <UNIX_SECONDS>" \
-H "webhook-signature: v1,<SIGNATURE>" \
-d "$BODY" \
"https://<BOX_HOST>/api/traffic/v1/feeds/watchlist/1"
Tanda tangan yang salah, stempel waktu yang selisihnya terlalu jauh, id yang berulang dan lokasi tanpa kunci semuanya
dijawab 401.
1.0.0. Diperbarui 2026-11-06