DokumentasiENarsa.technology

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, 429 atau 5xx, atau tanpa jawaban, dicoba lagi dengan jeda yang makin panjang: 30 detik, berlipat dua hingga satu jam, atau waktu di header Retry-After Anda. Pesan tetap berurutan: pesan berikutnya menunggu di belakang pesan yang sedang dicoba ulang.
  • Setelah 24 jam pesan yang masih gagal dihentikan, dan health langganan menunjukkan failing.
  • Jawaban lain: 4xx lainnya, 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}/retry mengirim 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-id yang 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.

Apakah halaman ini membantu?

1.0.0. Diperbarui 2026-11-06