API v2 (beta)
Webhook
Endpoint webhook adalah URL di server Anda yang dipanggil WbizTool saat terjadi sesuatu pada pesan: pesan terkirim, gagal, dibatalkan, atau kedaluwarsa. Anda langsung mendapat kabarnya tanpa polling, dan setiap request ditandatangani sehingga Anda dapat memastikan request itu berasal dari WbizTool.
| Operasi | Endpoint | Scope |
|---|---|---|
| Menambahkan endpoint | POST /api/v2/webhooks | messages:read |
| Daftar endpoint | GET /api/v2/webhooks | messages:read |
| Mengambil endpoint | GET /api/v2/webhooks/{id} | messages:read |
| Menghapus endpoint | DELETE /api/v2/webhooks/{id} | messages:read |
| Mengganti rahasia penandatanganan | POST /api/v2/webhooks/{id}/rotate-secret | messages:read |
| Mengirim peristiwa uji | POST /api/v2/webhooks/{id}/test | messages:read |
Endpoint dimiliki oleh workspace dan menerima peristiwa untuk semua pesannya, apa pun yang mengirimnya: API, dashboard, kampanye, pengingat, atau integrasi. Karena itu kunci yang dibatasi ke sebagian nomor tidak dapat mengelolanya: setiap panggilan dijawab dengan 403 number_not_allowed.
Peristiwa#
| Jenis | Kapan |
|---|---|
message.sent | Pesan terkirim dari nomor Anda. |
message.failed | Pesan tidak dapat dikirim; error.message menyebutkan alasannya. |
message.cancelled | Pesan yang menunggu dibatalkan: dengan API, dari dashboard, atau karena nomor atau kampanyenya dihapus. |
message.expired | Pesan tidak terkirim sebelum expires_at-nya. |
ping | Peristiwa uji yang Anda minta. |
Peristiwa untuk nomor (terhubung, ter-logout) dan untuk pesan masuk akan menyusul. Centang terkirim dan dibaca tidak mengirim peristiwa; ambil pesannya untuk melihatnya.
Menambahkan endpoint#
https://wbiztool.com/api/v2/webhooksBody: JSON (application/json)
Body
urlstringwajibEndpoint Anda: alamat
https://publik, hingga 500 karakter. Alamathttp://, privat, dan lokal (sepertilocalhostatau192.168.x.x) ditolak.eventsarray of stringsopsionalJenis peristiwa yang diterimanya: salah satu atau beberapa dari
message.sent,message.failed,message.cancelled, danmessage.expired. Default: keempatnya.pingselalu datang saat Anda memintanya.descriptionstringopsionalCatatan untuk Anda sendiri, hingga 100 karakter, seperti
Order system.
Satu workspace dapat memiliki hingga 5 endpoint. Responsnya 201 dengan endpoint beserta secret penandatanganannya, yang hanya ditampilkan kali ini. Simpan di tempat yang dapat dibaca penerima Anda, seperti environment variable. Respons berikutnya hanya menampilkan secret_last4; jika rahasianya hilang, ganti rahasianya.
curl -X POST https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/wbiztool/events",
"events": ["message.sent", "message.failed"],
"description": "Order system"
}'import os
import requests
response = requests.post(
"https://wbiztool.com/api/v2/webhooks",
headers={"Authorization": f"Bearer {os.environ['WBIZTOOL_API_KEY']}"},
json={
"url": "https://example.com/wbiztool/events",
"events": ["message.sent", "message.failed"],
"description": "Order system",
},
timeout=30,
)
endpoint = response.json()
print("Store this secret now:", endpoint["secret"])// Node.js 18+ (built-in fetch). Save as .mjs to use top-level await.
const response = await fetch("https://wbiztool.com/api/v2/webhooks", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.WBIZTOOL_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example.com/wbiztool/events",
events: ["message.sent", "message.failed"],
description: "Order system",
}),
});
const endpoint = await response.json();
console.log("Store this secret now:", endpoint.secret);<?php
$ch = curl_init('https://wbiztool.com/api/v2/webhooks');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WBIZTOOL_API_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/wbiztool/events',
'events' => ['message.sent', 'message.failed'],
'description' => 'Order system',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$endpoint = json_decode(curl_exec($ch), true);
curl_close($ch);
echo 'Store this secret now: ' . $endpoint['secret'];Respons (201):
{
"id": 41,
"url": "https://example.com/wbiztool/events",
"description": "Order system",
"events": ["message.sent", "message.failed"],
"secret_last4": "9c1e",
"active": true,
"disabled_reason": null,
"created_at": "2026-10-18T11:20:05.733Z",
"last_delivery": null,
"secret": "3f0b…9c1e"
}
secret yang sebenarnya terdiri dari 64 karakter heksadesimal. Percobaan ulang request ini dengan Idempotency-Key yang sama tidak menambahkan endpoint kedua, dan responsnya tidak menyertakan rahasia.
Error saat menambahkan endpoint#
| HTTP | Kode | Kapan |
|---|---|---|
400 | validation_failed | url tidak ada atau terlalu panjang, events kosong atau berisi jenis yang tidak dikenal, atau ada field yang tidak dikenal |
403 | webhook_limit_reached | Workspace sudah memiliki 5 endpoint |
403 | number_not_allowed | Kunci dibatasi ke sebagian nomor |
422 | invalid_webhook_url | url bukan alamat https:// publik |
Daftar endpoint#
https://wbiztool.com/api/v2/webhooksEndpoint workspace yang menerima peristiwa, yang terlama lebih dulu, dalam satu daftar data.
curl https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
Daftar ini tidak menyertakan endpoint yang sudah berhenti. Pemilik juga dapat melihat endpoint, dan menghapusnya, di Pengaturan → Kunci API pada bagian Endpoint webhook.
Mengambil endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Satu endpoint. last_delivery menunjukkan hasil pengiriman terakhirnya. Endpoint yang tidak ada atau sudah dihapus dijawab dengan 404 webhook_not_found; endpoint yang sudah berhenti ditampilkan dengan active: false.
Menghapus endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Tidak ada lagi yang dikirim ke endpoint itu, termasuk percobaan ulang yang masih menunggu. Responsnya {"id": 41, "deleted": true}.
Mengganti rahasia penandatanganan#
https://wbiztool.com/api/v2/webhooks/{id}/rotate-secretMembuat rahasia baru untuk endpoint. Responsnya 200 dengan endpoint beserta secret barunya, yang hanya ditampilkan kali ini. Rahasia lama langsung berhenti berlaku: peristiwa yang dikirim setelah ini ditandatangani dengan rahasia baru, termasuk percobaan ulang peristiwa sebelumnya. Segera perbarui penerima Anda.
curl -X POST https://wbiztool.com/api/v2/webhooks/41/rotate-secret \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
Mengirim peristiwa uji#
https://wbiztool.com/api/v2/webhooks/{id}/testMemasukkan peristiwa ping ke antrean untuk endpoint ini, ditandatangani seperti setiap peristiwa, sehingga Anda dapat menguji penerima Anda. Responsnya 202:
{ "event_id": "evt_8a2f4c1d9b7e6a5f4c3b2a19", "type": "ping", "queued": true }
Endpoint Anda menerima:
{
"id": "evt_8a2f4c1d9b7e6a5f4c3b2a19",
"type": "ping",
"created_at": "2026-10-18T11:21:40.002Z",
"data": { "webhook_id": 41 }
}
Payload peristiwa#
Setiap peristiwa berupa POST dengan body JSON:
{
"id": "evt_c41d7e2a9f3b8c6d5e4f3a21",
"type": "message.sent",
"created_at": "2026-10-18T10:15:09.912Z",
"data": {
"message": {
"id": 9817263,
"status": "sent",
"from": { "id": 101, "phone": "919800000000", "platform": "whatsapp" },
"to": { "phone": "+919876543210", "contact_id": 991 },
"text": "Hi Asha, your order #1042 is ready.",
"media": null,
"send_at": null,
"expires_at": null,
"created_at": "2026-10-18T10:15:02.311Z",
"sent_at": "2026-10-18T10:15:09.870Z",
"error": null
}
}
}
| Field | Keterangan |
|---|---|
id | ID peristiwa, evt_ diikuti 24 karakter. Peristiwa yang sama mempertahankan ID-nya di setiap percobaan ulang. |
type | Salah satu jenis peristiwa. |
created_at | Kapan peristiwa terjadi. |
data.message | Untuk peristiwa message.*: Message sebagaimana kondisinya saat itu. media.url di dalamnya berfungsi setidaknya selama 26 jam. |
data.webhook_id | Untuk ping: ID endpoint. |
Header:
| Header | Nilai |
|---|---|
Content-Type | application/json |
User-Agent | WbizTool-Webhooks/2 |
WbizTool-Event-Id | id peristiwa, sama di setiap percobaan ulang |
WbizTool-Timestamp | Kapan percobaan ini dikirim, dalam detik Unix, seperti 1792310409 |
WbizTool-Signature | v1= diikuti HMAC-SHA256 heksadesimal dari <timestamp>.<raw body>, dibuat dengan rahasia endpoint |
Memverifikasi tanda tangan#
Periksa setiap request sebelum Anda memercayainya: hitung tanda tangan dari body mentah (sebelum framework Anda mengurai JSON-nya), bandingkan dengan header dalam waktu konstan, dan tolak timestamp yang selisihnya lebih dari 5 menit, agar request lama tidak dapat diputar ulang. Skemanya sama dengan tanda tangan webhook API v1, dengan rahasia milik endpoint itu sendiri.
Lalu gunakan WbizTool-Event-Id untuk membuang duplikat: peristiwa yang sama dapat datang lebih dari sekali, misalnya saat respons Anda hilang dan kami mencoba lagi.
import hashlib, hmac, json, os, time
SECRET = os.environ["WBIZTOOL_WEBHOOK_SECRET"]
def verify(secret: str, body: bytes, headers) -> bool:
ts, sig = headers.get("WbizTool-Timestamp", ""), headers.get("WbizTool-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
mac = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig)
# Flask
from flask import Flask, request
app = Flask(__name__)
seen = set() # use your database in production
@app.post("/wbiztool/events")
def events():
body = request.get_data() # the raw bytes
if not verify(SECRET, body, request.headers):
return "", 401
event_id = request.headers["WbizTool-Event-Id"]
if event_id in seen:
return "", 200 # a repeat: already handled
seen.add(event_id)
event = json.loads(body)
if event["type"] == "message.failed":
message = event["data"]["message"]
print("Failed:", message["id"], message["error"]["message"])
return "", 200Di Django, berikan request.body dan request.headers.
const crypto = require("crypto");
const express = require("express");
const SECRET = process.env.WBIZTOOL_WEBHOOK_SECRET;
function verify(secret, rawBody, headers) {
const ts = String(headers["wbiztool-timestamp"] || ""), sig = String(headers["wbiztool-signature"] || "");
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = "v1=" + crypto.createHmac("sha256", secret).update(ts + ".").update(rawBody).digest("hex");
return sig.length === mac.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mac));
}
const app = express();
const seen = new Set(); // use your database in production
// express.raw keeps the body as a Buffer: the signature covers the exact bytes.
app.post("/wbiztool/events", express.raw({ type: "*/*" }), (req, res) => {
if (!verify(SECRET, req.body, req.headers)) return res.sendStatus(401);
const eventId = req.headers["wbiztool-event-id"];
if (seen.has(eventId)) return res.sendStatus(200); // a repeat: already handled
seen.add(eventId);
const event = JSON.parse(req.body);
if (event.type === "message.failed") {
const message = event.data.message;
console.log("Failed:", message.id, message.error.message);
}
res.sendStatus(200);
});
app.listen(3000);Jawab 2xx dengan cepat, lalu kerjakan proses yang lambat setelahnya (misalnya di antrean).
Percobaan ulang#
- Jawab dengan
2xxapa pun dalam 10 detik. Selain itu dianggap gagal: kode status lain (termasuk4xx), tidak ada jawaban dalam 10 detik, atau tidak ada koneksi dalam 3 detik. Redirect diikuti, paling banyak 3 kali, dan masing-masing harus mengarah ke alamat publik. - Pengiriman yang gagal dicoba lagi setelah 1, 5, dan 15 menit, lalu 1, 2, 4, 8, dan 8 jam: 9 percobaan dalam sekitar 23 jam. Setelah percobaan terakhir, peristiwa itu dibuang.
- Percobaan ulang memiliki body dan
WbizTool-Event-Idyang sama, denganWbizTool-Timestampdan tanda tangan yang baru. - Urutan tidak dijamin.
message.sentyang dicoba ulang dapat datang setelah peristiwa yang lebih baru. Gunakanstatuspesan, atau ambil pesannya, jika urutan penting.
last_delivery pada endpoint menunjukkan hasil pengiriman terakhir: state (pending, delivered, atau failed), status_code yang dijawab server Anda, dan kapan peristiwa dibuat.
Kapan endpoint berhenti#
Endpoint berhenti menerima peristiwa jika salah satu hal berikut terjadi. Endpoint tetap ada di GET /webhooks dengan active: false dan disabled_reason, sehingga Anda dapat melihat alasannya; endpoint masih dapat dibaca dan dihapus, tetapi tidak dapat diuji atau diberi rahasia baru (404).
- Gagal selama 24 jam. Jika tidak ada yang berhasil dikirim ke endpoint itu selama 24 jam sementara pengirimannya terus gagal, endpoint dinonaktifkan dan percobaan ulang yang menunggu dibuang. Pengaturan → Kunci API kemudian menampilkannya sebagai Nonaktif. Perbaiki server Anda, lalu tambahkan endpoint lagi.
- Kunci yang membuatnya dicabut atau kedaluwarsa. Mencabut kunci menghentikan semua yang disiapkannya. Tambahkan endpoint lagi dengan kunci lain.
- Endpoint dihapus, dengan Menghapus endpoint atau di Pengaturan → Kunci API.
Endpoint yang sudah berhenti tidak dihitung dalam batas 5.
Objek endpoint webhook#
| Field | Jenis | Keterangan |
|---|---|---|
id | integer | ID endpoint. |
url | string | Tujuan pengiriman peristiwa. |
description | string | Catatan Anda; "" jika tidak ada. |
events | array | Jenis peristiwa yang diterimanya. |
secret | string | Rahasia penandatanganan: hanya ada di respons Menambahkan endpoint dan Mengganti rahasia penandatanganan. |
secret_last4 | string | 4 karakter terakhir rahasia, untuk mengetahui rahasia mana yang Anda miliki. |
active | boolean | false jika endpoint tidak lagi menerima peristiwa – lihat Kapan endpoint berhenti. |
disabled_reason | string or null | Alasan berhenti (gagal selama 24 jam, atau kunci yang membuatnya dicabut atau kedaluwarsa); null selama aktif. |
created_at | string | Kapan endpoint ditambahkan. |
last_delivery | object or null | Pengiriman terakhir yang dicoba: state, status_code (atau null jika server Anda tidak menjawab), dan created_at. null sebelum pengiriman pertama. |
Terkait#
- Pesan: objek Message di
data.message - Tanda tangan webhook untuk API v1
- Error
